├── tests ├── __init__.py ├── test_approx.py ├── test_pep542.py ├── test_repeat.py ├── test_switch.py ├── test_where.py ├── test_decrement.py ├── test_french.py ├── test_increment.py ├── test_int_seq.py ├── test_nobreak.py ├── test_print_keyword.py ├── test_function.py ├── test_multiple_transforms.py ├── common.py ├── function_testfile.py ├── repeat_testfile.py ├── multiple_testfile.py ├── nobreak_testfile.py ├── pep542_testfile.py ├── where_testfile.py ├── french_testfile.py ├── spanish_testfile.py ├── decrement_testfile.py ├── increment_testfile.py ├── print_testfile.py ├── approx_testfile.py ├── switch_testfile.py ├── test_a_console.py ├── readme.md └── int_seq_testfile.py ├── experimental ├── __main__.py ├── core │ ├── __init__.py │ ├── console.py │ ├── import_hook.py │ └── transforms.py ├── transformers │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ ├── shared.py │ │ ├── one2one.py │ │ └── simple2to3.py │ ├── print_keyword.py │ ├── nobreak_keyword.py │ ├── convert_py2.py │ ├── function_keyword.py │ ├── where_clause.py │ ├── french_syntax.py │ ├── increment.py │ ├── decrement.py │ ├── spanish_syntax.py │ ├── pep542.py │ ├── repeat_keyword.py │ ├── switch_statement.py │ ├── approx.py │ ├── int_seq.py │ └── readme.md ├── version.py └── __init__.py ├── pypi_upload.bat ├── setup.py ├── LICENSE ├── tools └── create_transforms_readme.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /experimental/__main__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /experimental/core/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /experimental/transformers/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /experimental/transformers/utils/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /experimental/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.7" 2 | -------------------------------------------------------------------------------- /tests/test_approx.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .approx_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_pep542.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .pep542_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_repeat.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .repeat_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_switch.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .switch_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_where.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .where_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_decrement.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .decrement_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_french.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .french_testfile import * 3 | 4 | -------------------------------------------------------------------------------- /tests/test_increment.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .increment_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_int_seq.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .int_seq_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_nobreak.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .nobreak_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_print_keyword.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .print_testfile import * 3 | -------------------------------------------------------------------------------- /tests/test_function.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .function_testfile import * 3 | 4 | -------------------------------------------------------------------------------- /tests/test_multiple_transforms.py: -------------------------------------------------------------------------------- 1 | from .common import experimental 2 | from .multiple_testfile import * 3 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 4 | 5 | import experimental 6 | -------------------------------------------------------------------------------- /experimental/transformers/utils/shared.py: -------------------------------------------------------------------------------- 1 | '''An empty module whose dict will be populated by other modules when needed. 2 | Used mostly as a means to communicate required functions to the console. 3 | ''' 4 | shared_dict = {} 5 | alpha = "string" 6 | -------------------------------------------------------------------------------- /tests/function_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import function_keyword 2 | 3 | def test_function(): 4 | square = function x: x**2 5 | assert square(3) == 9 6 | 7 | if __name__ == "__main__": 8 | test_function() 9 | print("Success.") 10 | -------------------------------------------------------------------------------- /tests/repeat_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import repeat_keyword 2 | 3 | def test_repeat(): 4 | i = 1 5 | repeat 4: 6 | i += 1 7 | assert i == 5 8 | 9 | if __name__ == "__main__": 10 | test_repeat() 11 | print("Success.") 12 | -------------------------------------------------------------------------------- /pypi_upload.bat: -------------------------------------------------------------------------------- 1 | echo off 2 | :Ask 3 | echo Did you update the version?(y/n) 4 | set ANSWER= 5 | set /P ANSWER=Type input: %=% 6 | If /I "%ANSWER%"=="y" goto yes 7 | If /I "%ANSWER%"=="n" goto no 8 | echo Incorrect input & goto Ask 9 | :yes 10 | del /Q dist\*.* 11 | python setup.py sdist bdist_wheel 12 | twine upload dist/* 13 | :no 14 | -------------------------------------------------------------------------------- /tests/multiple_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import decrement, increment 2 | from __experimental__ import french_syntax 3 | 4 | def test_increment_decrement(): 5 | a = 0 6 | a-- 7 | a ++ 8 | assert a == 0 9 | 10 | def test_french(): 11 | assert Vrai 12 | 13 | if __name__ == "__main__": 14 | test_increment_decrement() 15 | test_french() 16 | print("Success.") 17 | -------------------------------------------------------------------------------- /tests/nobreak_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import nobreak_keyword 2 | 3 | def test_nobreak(): 4 | x = 1 5 | for i in range(3): 6 | x += 1 7 | nobreak: 8 | x = 42 9 | assert x == 42 10 | 11 | x = 1 12 | for i in range(3): 13 | if i == 1: 14 | break 15 | nobreak: 16 | x = 42 17 | assert x == 1 18 | 19 | if __name__ == "__main__": 20 | test_nobreak() 21 | print("Success.") 22 | -------------------------------------------------------------------------------- /tests/pep542_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import pep542 2 | 3 | def test_pep542(): 4 | class MyClass: 5 | pass 6 | 7 | def MyClass.square(self, x): 8 | return x**2 9 | 10 | my_instance = MyClass() 11 | 12 | def my_instance.out(): 13 | return 42 14 | 15 | assert my_instance.out() == 42 16 | assert my_instance.square(3) == 9 17 | 18 | if __name__ == "__main__": 19 | test_pep542() 20 | print("Success.") 21 | -------------------------------------------------------------------------------- /tests/where_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import where_clause 2 | 3 | def next(i): 4 | return i+1 5 | 6 | 7 | def twice(i, next): 8 | where: 9 | i: int 10 | next: Function[[int], int] 11 | return: int 12 | return next(next(i)) 13 | 14 | def test_where(): 15 | i = 3 16 | where: 17 | i: int 18 | assert twice(i, next) == 5 19 | 20 | if __name__ == "__main__": 21 | test_where() 22 | print("Success.") 23 | -------------------------------------------------------------------------------- /tests/french_testfile.py: -------------------------------------------------------------------------------- 1 | ''' Just testing a small subset of all the French syntax ''' 2 | 3 | from __experimental__ import french_syntax 4 | 5 | de math importe pi 6 | 7 | def test_bool(): 8 | assert Vrai, "Vrai is True" 9 | assert not Faux, "Faux is False" 10 | 11 | def test_for(): 12 | total = 0 13 | pour i dans intervalle(10): 14 | total += i 15 | assert total == 45 16 | 17 | if __name__ == "__main__": 18 | test_bool() 19 | test_for() 20 | print("Success.") 21 | -------------------------------------------------------------------------------- /tests/spanish_testfile.py: -------------------------------------------------------------------------------- 1 | ''' Just testing a small subset of all the Spanish syntax ''' 2 | 3 | from __experimental__ import spanish_syntax 4 | 5 | de math importar pi 6 | 7 | def test_bool(): 8 | assert Verdadero, "Verdadero is True" 9 | assert not Falso, "Falso is False" 10 | 11 | def test_for(): 12 | total = 0 13 | para i en intervalo(10): 14 | total += i 15 | assert total == 45 16 | 17 | if __name__ == "__main__": 18 | test_bool() 19 | test_for() 20 | print("Success.") 21 | -------------------------------------------------------------------------------- /tests/decrement_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import decrement 2 | 3 | def test_decrement(): 4 | a = 2 5 | a-- 6 | assert a == 1 7 | 8 | def test_decrement_after_colon(): 9 | usual = 10 10 | for _ in range(3): usual -= 1 11 | if True: usual -= 1 12 | assert usual == 6 13 | 14 | unusual = 10 15 | for _ in range(3): unusual-- 16 | if True: unusual-- 17 | assert unusual == 6 18 | 19 | if __name__ == "__main__": 20 | test_decrement() 21 | test_decrement_after_colon() 22 | print("Success.") 23 | -------------------------------------------------------------------------------- /tests/increment_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import increment 2 | 3 | def test_increment(): 4 | a = 0 5 | a++ 6 | assert a == 1 7 | 8 | def test_increment_after_colon(): 9 | usual = 0 10 | for _ in range(3): usual += 1 11 | if True: usual += 1 12 | assert usual == 4 13 | 14 | unusual = 0 15 | for _ in range(3): unusual++ 16 | if True: unusual++ 17 | assert unusual == 4 18 | 19 | if __name__ == "__main__": 20 | test_increment() 21 | test_increment_after_colon() 22 | print("Success.") 23 | -------------------------------------------------------------------------------- /tests/print_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import print_keyword 2 | 3 | import sys 4 | 5 | class MyOutput: 6 | def __init__(self): 7 | self.out = [] 8 | def write(self, text): 9 | self.out.append(text) 10 | def flush(self): 11 | self.out = [] 12 | 13 | def test_print(): 14 | original = sys.stdout 15 | sys.stdout = output = MyOutput() 16 | print "Hello World!" 17 | assert output.out == ["Hello World!", "\n"] 18 | output.flush() 19 | print 20 | assert output.out == ["\n"] 21 | sys.stdout = original 22 | 23 | if __name__ == "__main__": 24 | test_print() 25 | print("Success.") 26 | -------------------------------------------------------------------------------- /experimental/transformers/print_keyword.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import print_keyword 2 | 3 | triggers the use of the lib2to3 Python library to automatically convert 4 | all `print` statements (assumed to use the Python 2 syntax) into 5 | function calls. 6 | ''' 7 | 8 | from utils.simple2to3 import MyRefactoringTool, get_single_fixer 9 | 10 | try: 11 | my_fixes = MyRefactoringTool( [get_single_fixer("print")] ) 12 | except: 13 | print("Cannot create MyRefactoringTool in print_keyword.") 14 | my_fixes = None 15 | 16 | def transform_source(source): 17 | if my_fixes is None: 18 | return source 19 | return my_fixes.refactor_source(source) 20 | 21 | -------------------------------------------------------------------------------- /experimental/transformers/nobreak_keyword.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import nobreak_keyword 2 | 3 | enables to use the fake keyword `nobreak` instead of `else`, as in 4 | 5 | for i in range(3): 6 | print(i) 7 | nobreak: 8 | print("The entire loop was run.") 9 | 10 | Note that `nobreak` can be use everywhere `else` could be used, 11 | (including in `if` blocks) even if would not make sense. 12 | 13 | The transformation is done using the tokenize module; it should 14 | only affect code and not content of strings. 15 | ''' 16 | 17 | from utils.one2one import translate 18 | 19 | def transform_source(source): 20 | return translate(source, {'nobreak': 'else'}) 21 | 22 | -------------------------------------------------------------------------------- /experimental/transformers/convert_py2.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import convert_py2 2 | 3 | triggers the use of the lib2to3 Python library to automatically convert 4 | the code from Python 2 to Python 3 prior to executing it. 5 | 6 | As long as lib2to3 can convert the code, this means that code written 7 | using Python 2 syntax can be run using a Python 3 interpreter. 8 | ''' 9 | 10 | 11 | from utils.simple2to3 import MyRefactoringTool, get_lib2to3_fixers 12 | 13 | try: 14 | my_fixes = MyRefactoringTool(get_lib2to3_fixers()) 15 | except: 16 | print("Cannot create MyRefactoringTool in convert_py2.") 17 | my_fixes = None 18 | 19 | 20 | def transform_source(source): 21 | if my_fixes is None: 22 | return source 23 | return my_fixes.refactor_source(source) 24 | -------------------------------------------------------------------------------- /experimental/transformers/utils/one2one.py: -------------------------------------------------------------------------------- 1 | '''This module contains a single function: translate. 2 | 3 | Using the tokenize module, this function parses some source code 4 | an apply a translation based on a one-to-one translation table 5 | represented by a Python dict. 6 | ''' 7 | 8 | from io import StringIO 9 | import tokenize 10 | 11 | 12 | def translate(source, dictionary): 13 | '''A dictionary with a one-to-one translation of keywords is used 14 | to provide the transformation. 15 | ''' 16 | toks = tokenize.generate_tokens(StringIO(source).readline) 17 | result = [] 18 | for toktype, tokvalue, _, _, _ in toks: 19 | if toktype == tokenize.NAME and tokvalue in dictionary: 20 | result.append((toktype, dictionary[tokvalue])) 21 | else: 22 | result.append((toktype, tokvalue)) 23 | return tokenize.untokenize(result) 24 | -------------------------------------------------------------------------------- /tests/approx_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import approx 2 | 3 | def test_assert(): 4 | abs_tol = rel_tol = 1e-8 5 | assert 0.1 + 0.2 != 0.3 6 | assert 0.1 + 0.2 ~= 0.3, "test approximately equal" 7 | assert 0.1 + 0.2 ~= 0.3 # no message, but comment 8 | assert 0.1 + 0.2 <~= 0.3, 'test less than or approximately equal' 9 | assert 0.1 + 0.2 >~= 0.3, 'test greater than or approximately equal' 10 | 11 | 12 | def test_if(): 13 | abs_tol = rel_tol = 1e-8 14 | if 0.1 + 0.2 ~= 0.3: 15 | pass 16 | else: 17 | raise SyntaxError 18 | 19 | 20 | def test_missing_parameter(): 21 | try: 22 | assert 0.1 + 0.2 ~= 0.3 23 | raise SyntaxError 24 | except NameError: 25 | pass 26 | 27 | rel_tol = 1e-8 28 | try: 29 | assert 0.1 + 0.2 ~= 0.3 30 | raise SyntaxError 31 | except NameError: 32 | pass 33 | 34 | abs_tol = 1e-8 35 | assert 0.1 + 0.2 ~= 0.3 36 | 37 | if __name__ == "__main__": 38 | test_assert() 39 | test_if() 40 | print("Success.") 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #pylint: skip-file 2 | from setuptools import setup, find_packages 3 | from distutils.util import convert_path 4 | 5 | ## converting readme for pypi 6 | from pypandoc import convert 7 | def convert_md(filename): 8 | return convert(filename, 'rst') 9 | 10 | version_path = convert_path('experimental/version.py') 11 | with open(version_path) as version_file: 12 | exec(version_file.read()) 13 | 14 | 15 | setup(name='experimental', 16 | version=__version__, 17 | description="Enables easy modification of Python's syntax on the fly.", 18 | long_description = convert_md('README.md'), 19 | classifiers=[ 20 | 'Development Status :: 4 - Beta', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Programming Language :: Python :: 3.4', 23 | 'Topic :: Software Development :: Interpreters', 24 | ], 25 | url='https://github.com/aroberge/experimental', 26 | author='André Roberge', 27 | author_email='Andre.Roberge@gmail.com', 28 | license='MIT', 29 | packages=find_packages(exclude=['dist', 'build', 'tools']), 30 | zip_safe=False) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 André Roberge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/switch_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import switch_statement 2 | 3 | def example(n): 4 | result = '' 5 | switch n: 6 | case 2: 7 | result += '2 is even and ' 8 | case 3, 5, 7: 9 | result += f'{n} is prime' 10 | break 11 | case 0: pass 12 | case 1: 13 | pass 14 | case 4, 6, 8, 9: 15 | result = f'{n} is not prime' 16 | break 17 | default: 18 | result = f'{n} is not a single digit integer' 19 | return result 20 | 21 | def test_switch(): 22 | assert example(0) == '0 is not prime' 23 | assert example(1) == '1 is not prime' 24 | assert example(2) == '2 is even and 2 is prime' 25 | assert example(3) == '3 is prime' 26 | assert example(4) == '4 is not prime' 27 | assert example(5) == '5 is prime' 28 | assert example(6) == '6 is not prime' 29 | assert example(7) == '7 is prime' 30 | assert example(8) == '8 is not prime' 31 | assert example(9) == '9 is not prime' 32 | assert example(10) == '10 is not a single digit integer' 33 | 34 | 35 | if __name__ == "__main__": 36 | test_switch() 37 | print("Success.") 38 | -------------------------------------------------------------------------------- /experimental/transformers/function_keyword.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import function_keyword 2 | 3 | enables to use the word `function` instead of `lambda`, as in 4 | 5 | square = function x: x**2 6 | 7 | square(3) # returns 9 8 | 9 | `lambda` can still be used in the source code. 10 | 11 | The transformation is done using the tokenize module; it should 12 | only affect code and not content of strings. 13 | ''' 14 | 15 | from utils.one2one import translate 16 | 17 | def transform_source(source): 18 | '''Replaces instances of 19 | function 20 | by 21 | lambda 22 | ''' 23 | return translate(source, {'function': 'lambda'}) 24 | 25 | 26 | if __name__ == '__main__': 27 | sample = '''square = function x: x**2''' 28 | 29 | comparison = '''square =lambda x :x **2 ''' 30 | 31 | if comparison == transform_source(sample): 32 | print("Transformation done correctly") 33 | else: 34 | print("Transformation done incorrectly") 35 | import difflib 36 | d = difflib.Differ() 37 | diff = d.compare(comparison.splitlines(), 38 | transform_source(sample).splitlines()) 39 | print('\n'.join(diff)) 40 | -------------------------------------------------------------------------------- /tools/create_transforms_readme.py: -------------------------------------------------------------------------------- 1 | '''This creates a readme.md file in the directory experimental/transformers 2 | by extracting the docstring of each transform found in that directory. 3 | ''' 4 | 5 | import os 6 | import sys 7 | 8 | target_dir = os.path.abspath(os.path.join( 9 | os.path.dirname(__file__), '..', "experimental/transformers")) 10 | 11 | os.chdir(target_dir) 12 | sys.path.insert(0, target_dir) 13 | 14 | docstrings = [''' 15 | Most of the content of this readme has been automatically extracted from 16 | the docstring of each file found in this directory. 17 | 18 | Note that multiple transforms can be used in a single file, e.g. 19 | 20 | ```python 21 | from __experimental__ import increment, decrement 22 | from __experimental__ import function_keyword 23 | ``` 24 | '''] 25 | 26 | for f in os.listdir('.'): 27 | if f.startswith("_") or os.path.isdir(f) or not f.endswith(".py"): 28 | continue 29 | 30 | name = f[:-3] 31 | script = __import__(name) 32 | docstrings.append("## %s " % f) 33 | if script.__doc__ is None: 34 | script.__doc__ = "Docstring missing." 35 | docstrings.append(script.__doc__) 36 | 37 | with open("readme.md", "w") as readme: 38 | readme.write("\n\n".join(docstrings)) 39 | 40 | -------------------------------------------------------------------------------- /experimental/transformers/utils/simple2to3.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lib2to3.refactor import RefactoringTool 3 | import lib2to3.fixes as fixer_dir 4 | 5 | # This simple module appears to be incompatible with the import 6 | # hook. For this reason, it is important to wrap calls to it 7 | # with a try/except clause. I have found that a bare except, 8 | # that catches all errors, is definitely the best way to proceed. 9 | 10 | def get_lib2to3_fixers(): 11 | '''returns a list of all fixers found in the lib2to3 library''' 12 | fixers = [] 13 | fixer_dirname = fixer_dir.__path__[0] 14 | for name in sorted(os.listdir(fixer_dirname)): 15 | if name.startswith("fix_") and name.endswith(".py"): 16 | fixers.append("lib2to3.fixes." + name[:-3]) 17 | return fixers 18 | 19 | 20 | def get_single_fixer(fixname): 21 | '''return a single fixer found in the lib2to3 library''' 22 | fixer_dirname = fixer_dir.__path__[0] 23 | for name in sorted(os.listdir(fixer_dirname)): 24 | if (name.startswith("fix_") and name.endswith(".py") 25 | and fixname == name[4:-3]): 26 | return "lib2to3.fixes." + name[:-3] 27 | 28 | 29 | class MyRefactoringTool(RefactoringTool): 30 | '''This class must be instantiated with a list of all desired fixers''' 31 | _used_fixes = [] 32 | 33 | def __init__(self, fixer_names): 34 | # avoid duplicating fixers if called multiple times 35 | fixers = [fix for fix in fixer_names if fix not in self._used_fixes] 36 | super().__init__(fixers, options=None, explicit=None) 37 | self._used_fixes.extend(fixers) 38 | 39 | def refactor_source(self, source): 40 | source += "\n" # Silence certain parse errors 41 | tree = self.refactor_string(source, "original") 42 | return str(tree)[:-1] 43 | 44 | -------------------------------------------------------------------------------- /experimental/transformers/where_clause.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import where_clause 2 | 3 | shows how one could use `where` as a keyword to introduce a code 4 | block that would be ignored by Python. The idea was to use this as 5 | a _pythonic_ notation as an alternative for the optional type hinting described 6 | in PEP484. **This idea has been rejected** as it would not have 7 | been compatible with some older versions of Python, unlike the 8 | approach that has been accepted. 9 | https://www.python.org/dev/peps/pep-0484/#other-forms-of-new-syntax 10 | 11 | :warning: This transformation **cannot** be used in the console. 12 | 13 | For more details, please see two of my recent blog posts: 14 | 15 | https://aroberge.blogspot.ca/2015/12/revisiting-old-friend-yet-again.html 16 | 17 | https://aroberge.blogspot.ca/2015/01/type-hinting-in-python-focus-on.html 18 | 19 | I first suggested this idea more than 12 years ago! ;-) 20 | 21 | https://aroberge.blogspot.ca/2005/01/where-keyword-and-python-as-pseudo.html 22 | 23 | 24 | ''' 25 | 26 | from io import StringIO 27 | import tokenize 28 | 29 | NO_CONSOLE = '\nWarning: where_clause is not allowed in the console.\n' 30 | 31 | def transform_source(text): 32 | '''removes a "where" clause which is identified by the use of "where" 33 | as an identifier and ends at the first DEDENT (i.e. decrease in indentation)''' 34 | toks = tokenize.generate_tokens(StringIO(text).readline) 35 | result = [] 36 | where_clause = False 37 | for toktype, tokvalue, _, _, _ in toks: 38 | if toktype == tokenize.NAME and tokvalue == "where": 39 | where_clause = True 40 | elif where_clause and toktype == tokenize.DEDENT: 41 | where_clause = False 42 | continue 43 | 44 | if not where_clause: 45 | result.append((toktype, tokvalue)) 46 | return tokenize.untokenize(result) 47 | 48 | -------------------------------------------------------------------------------- /experimental/__init__.py: -------------------------------------------------------------------------------- 1 | #pylint: disable=C0103, W0212 2 | ''' 3 | In the following explanation, when we mention "the console" we refer to 4 | a session using the experimental interactive console included in this package. 5 | 6 | Possible invocations of this module: 7 | 8 | 1. python -m experimental: we want to start the console 9 | 2. python -m experimental script: we want to run "script" as the main program 10 | but do not want to start the console 11 | 3. python -i -m experimental script: we want to run "script" as the main program 12 | and we do want to start the console after 13 | script has ended 14 | 4. python -m experimental trans1 trans2 script: we want to run "script" as the 15 | main program, after registering the 16 | tansformers "trans1" and "trans2"; 17 | we do not want to start the console 18 | 5. python -i -m experimental trans1 trans2 script: same as 4 except that we 19 | want to start the console when script ends 20 | 21 | Note that a console is started in all cases except 4 above. 22 | ''' 23 | import sys 24 | 25 | from .core import console, import_hook, transforms 26 | start_console = console.start_console 27 | 28 | if "-m" in sys.argv: 29 | if len(sys.argv) > 1: 30 | for i in range(1, len(sys.argv)-1): 31 | transforms.import_transformer(sys.argv[i]) 32 | 33 | main_module = import_hook.import_main(sys.argv[-1]) 34 | 35 | if sys.flags.interactive: 36 | main_dict = {} 37 | for var in dir(main_module): 38 | if var in ["__cached__", "__loader__", 39 | "__package__", "__spec__"]: 40 | continue 41 | main_dict[var] = getattr(main_module, var) 42 | start_console(main_dict) 43 | else: 44 | start_console() 45 | -------------------------------------------------------------------------------- /experimental/transformers/french_syntax.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import french_syntax 2 | 3 | allows the use of a predefined subset of Python keyword to be written 4 | as their French equivalent; **English and French keywords can be mixed**. 5 | 6 | Thus, code like: 7 | 8 | si Vrai: 9 | imprime("French can be used.") 10 | autrement: 11 | print(Faux) 12 | 13 | Will be translated to 14 | 15 | if True: 16 | print("French can be used.") 17 | else: 18 | print(False) 19 | 20 | This type of transformation could be useful when teaching the 21 | very basic concepts of programming to (young) beginners who use 22 | non-ascii based language and would find it difficult to type 23 | ascii characters. 24 | 25 | The transformation is done using the tokenize module; it should 26 | only affect code and not content of strings. 27 | ''' 28 | 29 | from utils.one2one import translate 30 | 31 | def transform_source(source): 32 | '''Input text is assumed to contain some French equivalent words to 33 | normal Python keywords and a few builtin functions. 34 | These are transformed into normal Python keywords and functions. 35 | ''' 36 | # continue, def, global, lambda, nonlocal remain unchanged by choice 37 | 38 | dictionary = {'Faux': 'False', 'Aucun': 'None', 'Vrai': 'True', 39 | 'et': 'and', 'comme': 'as', 'affirme': 'assert', 40 | 'sortir': 'break', 'classe': 'class', 'élimine': 'del', 41 | 'ousi': 'elif', 'autrement': 'else', 'exception': 'except', 42 | 'finalement': 'finally', 'pour': 'for', 'de': 'from', 43 | 'si': 'if', 'importe': 'import', 'dans': 'in', 'est': 'is', 44 | 'non': 'not', 'ou': 'or', 'passe': 'pass', 45 | 'soulever': 'raise', 'retourne': 'return', 'essayer': 'try', 46 | 'pendant': 'while', 'avec': 'with', 'céder': 'yield', 47 | 'imprime': 'print', 'intervalle': 'range'} 48 | 49 | return translate(source, dictionary) 50 | -------------------------------------------------------------------------------- /experimental/transformers/increment.py: -------------------------------------------------------------------------------- 1 | ''' 2 | from __experimental__ import increment 3 | 4 | enables transformation of code of the form 5 | 6 | name ++ # optional comment 7 | other++ 8 | 9 | into 10 | 11 | name += 1 # optional comment 12 | other+= 1 13 | 14 | Space(s) betwen `name` and `++` are ignored. 15 | ''' 16 | 17 | from io import StringIO 18 | import tokenize 19 | 20 | 21 | def transform_source(src): 22 | toks = tokenize.generate_tokens(StringIO(src).readline) 23 | result = [] 24 | last_name = None 25 | last_plus = False 26 | for toktype, tokvalue, _, _, _ in toks: 27 | if toktype == tokenize.NAME: 28 | if last_name is not None: # two names in a row: not an increment 29 | result.append((tokenize.NAME, last_name)) 30 | result.append((tokenize.NAME, tokvalue)) 31 | last_name = None 32 | else: 33 | last_name = tokvalue 34 | elif last_name is not None: 35 | if toktype == tokenize.OP and tokvalue == '+': 36 | if last_plus: 37 | result.extend([ 38 | (tokenize.NAME, last_name), 39 | (tokenize.OP, '='), 40 | (tokenize.NAME, last_name), 41 | (tokenize.OP, '+'), 42 | (tokenize.NUMBER, '1') 43 | ]) 44 | last_plus = False 45 | last_name = None 46 | else: 47 | last_plus = True 48 | else: 49 | result.append((tokenize.NAME, last_name)) 50 | if last_plus: 51 | result.append((tokenize.OP, '+')) 52 | last_plus = False 53 | result.append((toktype, tokvalue)) 54 | last_name = None 55 | else: 56 | result.append((toktype, tokvalue)) 57 | 58 | if last_name: 59 | result.append((tokenize.NAME, last_name)) 60 | return tokenize.untokenize(result) 61 | -------------------------------------------------------------------------------- /experimental/transformers/decrement.py: -------------------------------------------------------------------------------- 1 | ''' 2 | from __experimental__ import decrement 3 | 4 | enables transformation of code of the form 5 | 6 | name -- # optional comment 7 | other-- 8 | 9 | into 10 | 11 | name -= 1 # optional comment 12 | other-= 1 13 | 14 | Space(s) betwen `name` and `--` are ignored. 15 | ''' 16 | 17 | from io import StringIO 18 | import tokenize 19 | 20 | 21 | def transform_source(src): 22 | toks = tokenize.generate_tokens(StringIO(src).readline) 23 | result = [] 24 | last_name = None 25 | last_minus = False 26 | for toktype, tokvalue, _, _, _ in toks: 27 | if toktype == tokenize.NAME: 28 | if last_name is not None: # two names in a row: not an increment 29 | result.append((tokenize.NAME, last_name)) 30 | result.append((tokenize.NAME, tokvalue)) 31 | last_name = None 32 | else: 33 | last_name = tokvalue 34 | elif last_name is not None: 35 | if toktype == tokenize.OP and tokvalue == '-': 36 | if last_minus: 37 | result.extend([ 38 | (tokenize.NAME, last_name), 39 | (tokenize.OP, '='), 40 | (tokenize.NAME, last_name), 41 | (tokenize.OP, '-'), 42 | (tokenize.NUMBER, '1') 43 | ]) 44 | last_minus = False 45 | last_name = None 46 | else: 47 | last_minus = True 48 | else: 49 | result.append((tokenize.NAME, last_name)) 50 | if last_minus: 51 | result.append((tokenize.OP, '-')) 52 | last_minus = False 53 | result.append((toktype, tokvalue)) 54 | last_name = None 55 | else: 56 | result.append((toktype, tokvalue)) 57 | 58 | if last_name: 59 | result.append((tokenize.NAME, last_name)) 60 | return tokenize.untokenize(result) 61 | -------------------------------------------------------------------------------- /experimental/transformers/spanish_syntax.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import spanish_syntax 2 | 3 | allows the use of a predefined subset of Python keyword to be written 4 | as their Spanish equivalent; **English and Spanish keywords can be mixed**. 5 | 6 | Neutral latin-american Spanish 7 | - translation by Sebastian Silva 8 | 9 | Thus, code like: 10 | 11 | si Verdadero: 12 | imprime("Spanish can be used.") 13 | sino: 14 | print(Falso) 15 | 16 | Will be translated to 17 | 18 | if True: 19 | print("Spanish can be used.") 20 | else: 21 | print(False) 22 | 23 | This type of transformation could be useful when teaching the 24 | very basic concepts of programming to (young) beginners who use 25 | non-ascii based language and would find it difficult to type 26 | ascii characters. 27 | 28 | The transformation is done using the tokenize module; it should 29 | only affect code and not content of strings. 30 | ''' 31 | 32 | from utils.one2one import translate 33 | 34 | def transform_source(source): 35 | '''Input text is assumed to contain some French equivalent words to 36 | normal Python keywords and a few builtin functions. 37 | These are transformed into normal Python keywords and functions. 38 | ''' 39 | # continue, def, global, lambda, nonlocal remain unchanged by choice 40 | 41 | dictionary = {'Falso': 'False', 'Nada': 'None', 'Verdadero': 'True', 42 | 'y': 'and', 'como': 'as', 'afirmar': 'assert', 43 | 'interrumpir': 'break', 'clase': 'class', 'eliminar': 'del', 44 | 'osi': 'elif', 'sino': 'else', 'excepto': 'except', 45 | 'finalmente': 'finally', 'para': 'for', 'de': 'from', 46 | 'si': 'if', 'importar': 'import', 'en': 'in', 'es': 'is', 47 | 'no': 'not', 'o': 'or', 'seguir': 'pass', 48 | 'elevar': 'raise', 'retornar': 'return', 'intentar': 'try', 49 | 'mientras': 'while', 'con': 'with', 'ceder': 'yield', 50 | 'imprimir': 'print', 'intervalo': 'range'} 51 | 52 | return translate(source, dictionary) 53 | -------------------------------------------------------------------------------- /tests/test_a_console.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0103 2 | import subprocess 3 | from .common import experimental 4 | 5 | banner = experimental.core.console.banner 6 | prompt = experimental.core.console.prompt 7 | 8 | ### sessions items: 9 | ### (command, input, expected_output, expected_error) 10 | 11 | sessions = [ 12 | ("python -m experimental", """ 13 | from __experimental__ import increment, decrement, print_keyword 14 | a = 3 15 | print a 16 | a++ 17 | print a 18 | a-- 19 | print a 20 | exit() 21 | """, """ 22 | 3 23 | 4 24 | 3 25 | """), 26 | ("python -i -m experimental tests.decrement_testfile", 27 | 'a = 7 \na--\nprint(a)', 28 | 'Success.\n6'), 29 | ("python -i -m experimental tests.french_testfile", 30 | 'Vrai', 31 | 'Success.\nTrue'), 32 | ("python -i -m experimental tests.function_testfile", 33 | 'sq = function x: x**2\nsq(3)', 34 | 'Success.\n9'), 35 | ("python -i -m experimental tests.increment_testfile", 36 | 'a = 7 \na++ \nprint(a)', 37 | 'Success.\n8'), 38 | ("python -i -m experimental tests.print_testfile", 39 | 'print 1', 'Success.\n1'), 40 | ("python -i -m experimental tests.repeat_testfile", 41 | 'repeat 2:\n print("*", end="")\n\n', 'Success.\n... ... **'), 42 | ] 43 | 44 | def compare_output(real, expected): 45 | '''The output from the console includes the prompt. 46 | To make tests less brittle and easier to write, we strip the prompt 47 | and remove leading and trailing spaces. 48 | ''' 49 | return real.replace(prompt, '').strip() == expected.strip() 50 | 51 | def test_console(): 52 | '''Function discoverable and run by pytest''' 53 | for command, inp, out in sessions: 54 | process = subprocess.Popen( 55 | command, 56 | shell=False, 57 | stdin=subprocess.PIPE, 58 | stdout=subprocess.PIPE, 59 | stderr=subprocess.PIPE, 60 | universal_newlines=True # use strings as input 61 | ) 62 | # I have found comparisons with stderr problematic, so I ignore it 63 | # However, since all tests are supposed not to raise exceptions 64 | # but only valid output, this does not impact the reliability 65 | # of these tests: if stdout is not as expected, we have a problem. 66 | stdout, _ = process.communicate(inp) 67 | process.wait() 68 | assert compare_output(stdout, out) 69 | 70 | 71 | if __name__ == "__main__": 72 | test_console() 73 | -------------------------------------------------------------------------------- /experimental/transformers/pep542.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import pep542 2 | 3 | Trying to implement https://www.python.org/dev/peps/pep-0542/ 4 | ''' 5 | 6 | from io import StringIO 7 | import tokenize 8 | 9 | def transform_source(text): 10 | toks = tokenize.generate_tokens(StringIO(text).readline) 11 | result = [] 12 | special_def_clause = False 13 | potential_prefix = None 14 | prefix = None 15 | begin_col = 0 16 | 17 | for toktype, tokvalue, begin, _, _ in toks: 18 | if not special_def_clause and toktype == tokenize.NAME and tokvalue == "def": 19 | special_def_clause = True # potentially 20 | begin_col = begin[1] 21 | elif special_def_clause and potential_prefix is None and prefix is None: 22 | potential_prefix = (toktype, tokvalue) 23 | continue 24 | elif special_def_clause and potential_prefix and prefix is None: 25 | if toktype == tokenize.OP and tokvalue == ".": 26 | prefix = potential_prefix 27 | continue 28 | else: # normal def 29 | result.append(potential_prefix) 30 | special_def_clause = False 31 | potential_prefix = None 32 | elif special_def_clause and potential_prefix and prefix: 33 | fn_name = tokvalue 34 | potential_prefix = None 35 | elif special_def_clause and not potential_prefix and prefix and (( 36 | toktype == tokenize.NAME and begin[1] == begin_col) or ( 37 | toktype == tokenize.ENDMARKER)): 38 | # insert special naming 39 | result.append(prefix) 40 | result.append((tokenize.OP, ".")) 41 | result.append((tokenize.NAME, fn_name)) 42 | result.append((tokenize.EQUAL, "=")) 43 | result.append((tokenize.NAME, fn_name)) 44 | result.append((tokenize.NEWLINE, "\n")) 45 | # remove from local variables 46 | result.append((tokenize.NAME, "del")) 47 | result.append((tokenize.NAME, fn_name)) 48 | result.append((tokenize.NEWLINE, "\n")) 49 | result.append((tokenize.NEWLINE, "\n")) 50 | 51 | result.append((toktype, tokvalue)) 52 | 53 | special_def_clause = False 54 | potential_prefix = None 55 | prefix = None 56 | begin_col = 0 57 | continue 58 | result.append((toktype, tokvalue)) 59 | 60 | return tokenize.untokenize(result) 61 | -------------------------------------------------------------------------------- /experimental/core/console.py: -------------------------------------------------------------------------------- 1 | #pylint: disable=W0102, C0103 2 | import code 3 | import platform 4 | import os 5 | import sys 6 | 7 | from . import transforms 8 | 9 | from .. import version 10 | 11 | # define banner and prompt here so that they can be imported in tests 12 | banner = "experimental console version {}. [Python version: {}]\n".format( 13 | version.__version__, platform.python_version()) 14 | prompt = "~~> " 15 | 16 | 17 | class ExperimentalInteractiveConsole(code.InteractiveConsole): 18 | '''A Python console that emulates the normal Python interpreter 19 | except that it support experimental code transformations.''' 20 | 21 | def push(self, line): 22 | """Transform and push a line to the interpreter. 23 | 24 | The line should not have a trailing newline; it may have 25 | internal newlines. The line is appended to a buffer and the 26 | interpreter's runsource() method is called with the 27 | concatenated contents of the buffer as source. If this 28 | indicates that the command was executed or invalid, the buffer 29 | is reset; otherwise, the command is incomplete, and the buffer 30 | is left as it was after the line was appended. The return 31 | value is 1 if more input is required, 0 if the line was dealt 32 | with in some way (this is the same as runsource()). 33 | 34 | """ 35 | if transforms.FROM_EXPERIMENTAL.match(line): 36 | transforms.add_transformers(line) 37 | self.buffer.append("\n") 38 | else: 39 | self.buffer.append(line) 40 | 41 | add_pass = False 42 | if line.rstrip(' ').endswith(":"): 43 | add_pass = True 44 | source = "\n".join(self.buffer) 45 | if add_pass: 46 | source += "pass" 47 | source = transforms.transform(source) 48 | if add_pass: 49 | source = source.rstrip(' ') 50 | if source.endswith("pass"): 51 | source = source[:-4] 52 | 53 | # some transformations may strip an empty line meant to end a block 54 | if not self.buffer[-1]: 55 | source += "\n" 56 | try: 57 | more = self.runsource(source, self.filename) 58 | except SystemExit: 59 | os._exit(1) 60 | 61 | if not more: 62 | self.resetbuffer() 63 | return more 64 | 65 | 66 | def start_console(local_vars={}): 67 | '''Starts a console; modified from code.interact''' 68 | transforms.CONSOLE_ACTIVE = True 69 | transforms.remove_not_allowed_in_console() 70 | sys.ps1 = prompt 71 | console = ExperimentalInteractiveConsole(locals=local_vars) 72 | console.interact(banner=banner) 73 | -------------------------------------------------------------------------------- /experimental/transformers/repeat_keyword.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import repeat_keyword 2 | 3 | introduces `repeat` as a keyword to write simple loops that repeat 4 | a set number of times. That is: 5 | 6 | repeat 3: 7 | a = 2 8 | repeat a*a: 9 | pass 10 | 11 | is equivalent to 12 | 13 | for __VAR_1 in range(3): 14 | a = 2 15 | for __VAR_2 in range(a*a): 16 | pass 17 | 18 | The names of the variables are chosen so as to ensure that they 19 | do not appear in the source code to be translated. 20 | 21 | The transformation is done using the tokenize module; it should 22 | only affect code and not content of strings. 23 | ''' 24 | 25 | from io import StringIO 26 | import tokenize 27 | 28 | 29 | def transform_source(text): 30 | '''Replaces instances of 31 | 32 | repeat n: 33 | by 34 | 35 | for __VAR_i in range(n): 36 | 37 | where __VAR_i is a string that does not appear elsewhere 38 | in the code sample. 39 | ''' 40 | 41 | loop_keyword = 'repeat' 42 | 43 | nb = text.count(loop_keyword) 44 | if nb == 0: 45 | return text 46 | 47 | var_names = get_unique_variable_names(text, nb) 48 | 49 | toks = tokenize.generate_tokens(StringIO(text).readline) 50 | result = [] 51 | replacing_keyword = False 52 | for toktype, tokvalue, _, _, _ in toks: 53 | if toktype == tokenize.NAME and tokvalue == loop_keyword: 54 | result.extend([ 55 | (tokenize.NAME, 'for'), 56 | (tokenize.NAME, var_names.pop()), 57 | (tokenize.NAME, 'in'), 58 | (tokenize.NAME, 'range'), 59 | (tokenize.OP, '(') 60 | ]) 61 | replacing_keyword = True 62 | elif replacing_keyword and tokvalue == ':': 63 | result.extend([ 64 | (tokenize.OP, ')'), 65 | (tokenize.OP, ':') 66 | ]) 67 | replacing_keyword = False 68 | else: 69 | result.append((toktype, tokvalue)) 70 | return tokenize.untokenize(result) 71 | 72 | 73 | ALL_NAMES = [] 74 | 75 | def get_unique_variable_names(text, nb): 76 | '''returns a list of possible variables names that 77 | are not found in the original text.''' 78 | base_name = '__VAR_' 79 | var_names = [] 80 | i = 0 81 | j = 0 82 | while j < nb: 83 | tentative_name = base_name + str(i) 84 | if text.count(tentative_name) == 0 and tentative_name not in ALL_NAMES: 85 | var_names.append(tentative_name) 86 | ALL_NAMES.append(tentative_name) 87 | j += 1 88 | i += 1 89 | return var_names 90 | 91 | if __name__ == '__main__': 92 | sample = '''# comment with repeat in it 93 | repeat 3: # first loop 94 | print('__VAR_1') 95 | repeat (2*2): 96 | pass''' 97 | 98 | comparison = '''# comment with repeat in it 99 | for __VAR_3 in range (3 ):# first loop 100 | print ('__VAR_1') 101 | for __VAR_2 in range ((2 *2 )): 102 | pass ''' 103 | 104 | transformed = transform_source(sample) 105 | if comparison == transformed: 106 | print("Transformation done correctly") 107 | else: 108 | print("Transformation done incorrectly") 109 | import difflib 110 | d = difflib.Differ() 111 | diff = d.compare(comparison.splitlines(), transformed.splitlines()) 112 | print('\n'.join(diff)) 113 | -------------------------------------------------------------------------------- /tests/readme.md: -------------------------------------------------------------------------------- 1 | # Important 2 | 3 | When running all the tests using pytest, the console test must run first; 4 | since pytest runs tests in alphabetical order, this is why it is named test_a_console.py 5 | 6 | # About these tests 7 | 8 | For simplicity, wherever possible, I follow [pytest](https://docs.pytest.org/en/latest/contents.html)'s strategy of simply using Python's `assert` statements and naming test files and test functions within these files all starting with `test_` for easy discovery. `pytest` is not part of Python's standard library and you may have to intall it. (If you use the Python Anaconda distribution from Continuum Analytics, it is likely already included.) 9 | 10 | However, since `experimental` changes the way `import` works, the _normal_ approach used by `pytest` is modified. For each test, two files are created: 11 | 12 | test_X.py 13 | X_testfile.py 14 | 15 | `X_testfile.py` [_see below for the naming convention_] is the actual file that contains the tests for the experimental, and almost always invalid Python syntax. As such, it cannot be imported by the normal mechanism. `test_X.py` imports `experimental`, which install an import hook. It then imports `X_testfile.py` which can be processed by the `experimental` import hook. 16 | 17 | `X_testfile.py`'s content should be something like: 18 | 19 | ```python 20 | from __experimental__ import X_feature 21 | 22 | def test_one(): 23 | ... 24 | assert something, "something is tested" 25 | ... 26 | 27 | def test_another(): 28 | ... 29 | assert something_else, "something else is tested" 30 | ... 31 | 32 | if __name__ == "__main__": 33 | test_one() 34 | test_another() 35 | print("Success.") 36 | ``` 37 | 38 | To ensure that the development version of `experimental` is used, `test_X.py` will first import a file (`common.py`) which changes `sys.path` to ensure that this is the case. 39 | 40 | ### About the naming convention 41 | 42 | Suppose I define a transformation named `X` found in file `X.py`. When creating test files in the `tests` directory, it is important not to have any file named `X.py` as well. Furthermore, pytest automatically loads files named `test_X.py` _and_ apparently also `X_test.py`. For this reason, if I need a file imported by `test_X.py`, I will often most often name it `X_testfile.py` so as to avoid any confusion. 43 | 44 | ## Problem with output capture 45 | 46 | When output needs to be captured, for example for tests performed with the transformation that enables the use of `print` as a keyword instead, the method used by pytest for capturing the output does not work as pytest will try to get the source of the file with experimental (and invalid) python syntax and execute it bypassing any transformations performed by `experimental`. In those cases, one must do the capture using a custom approach. An example of this can be seen in the file `print_keyword_test.py`. 47 | 48 | ## Using only pytest 49 | 50 | To write a series of tests that can be discovered by `pytest`, a file named `test_X` has only to contain two lines: 51 | 52 | from .common import experimental 53 | from .X_testfile import * 54 | 55 | The second line will ensure that functions named `test_X` are discovered by pytest. 56 | 57 | To run all tests from the parent directory containing the tests folder, the following can be used: 58 | 59 | python -m pytest tests 60 | 61 | 62 | ## Running a single test without pytest 63 | 64 | To run a single test file `X_testfile.py` without using pytest, we simply have to do 65 | 66 | python -m experimental path.to.X_testfile 67 | -------------------------------------------------------------------------------- /tests/int_seq_testfile.py: -------------------------------------------------------------------------------- 1 | from __experimental__ import int_seq 2 | 3 | 4 | def test_le_lt(): 5 | result = [] 6 | for x in 2 <= x < 7: # optional comment 7 | result.append(x) 8 | assert result == [2, 3, 4, 5, 6] 9 | 10 | def test_le_lt_paren(): 11 | result = [] 12 | for x in (2 <= x < 7): 13 | result.append(x) 14 | assert result == [2, 3, 4, 5, 6] 15 | 16 | def test_le_lt_cond(): 17 | result = [] 18 | for x in 2 <= x < 7 if x % 2 == 0:#another comment 19 | result.append(x) 20 | assert result == [2, 4, 6] 21 | 22 | 23 | def test_le_le(): 24 | result = [] 25 | for x in 2 <= x <= 7: 26 | result.append(x) 27 | assert result == [2, 3, 4, 5, 6, 7] 28 | 29 | 30 | def test_le_le_cond(): 31 | def is_even(n): 32 | return n % 2 == 0 33 | result = [] 34 | for x_ in 2 <= x_ <= 7 if is_even(x_): 35 | result.append(x_) 36 | assert result == [2, 4, 6] 37 | 38 | 39 | def test_lt_le(): 40 | result = [] 41 | for x in 2 < x <= 7: 42 | result.append(x) 43 | assert result == [3, 4, 5, 6, 7] 44 | 45 | 46 | def test_lt_le_cond(): 47 | result = [] 48 | for x in 2 < x <= 7 if x in [3, 5]: 49 | result.append(x) 50 | assert result == [3, 5] 51 | 52 | 53 | def test_lt_lt(): 54 | result = [] 55 | for x inseq 2 < x < 7: 56 | result.append(x) 57 | assert result == [3, 4, 5, 6] 58 | 59 | 60 | def test_lt_lt_cond(): 61 | result = [] 62 | for x in 2 < x < 7 if False: 63 | result.append(x) 64 | assert result == [] 65 | 66 | 67 | def test_ge_gt(): 68 | result = [] 69 | for x3 in 7 >= x3 > 2: 70 | result.append(x3) 71 | assert result == [7, 6, 5, 4, 3] 72 | 73 | 74 | def test_ge_gt_cond(): 75 | result = [] 76 | for x3 inseq 7 >= x3 > 2 if True: 77 | result.append(x3) 78 | assert result == [7, 6, 5, 4, 3] 79 | 80 | 81 | def test_ge_ge(): 82 | result = [] 83 | for other_ in 5 >= other_ >= 2: 84 | result.append(other_) 85 | assert result == [5, 4, 3, 2] 86 | 87 | 88 | def test_ge_ge_cond(): 89 | result = [] 90 | for other_ in 5 >= other_ >= 2 if other_ % 3: 91 | result.append(other_) 92 | assert result == [5, 4, 2] 93 | 94 | 95 | def test_gt_ge(): 96 | result = [] 97 | for _yx in -5 > _yx >= -8: 98 | result.append(_yx) 99 | assert result == [-6, -7, -8] 100 | 101 | 102 | def test_gt_ge_cond(): 103 | result = [] 104 | for _yx in -5 > _yx >= -8 if _yx != -7: 105 | result.append(_yx) 106 | assert result == [-6, -8] 107 | 108 | 109 | def test_gt_gt(): 110 | result = [] 111 | for x in 10 >= x >= 5: 112 | result.append(x) 113 | assert result == [10, 9, 8, 7, 6, 5] 114 | 115 | 116 | def test_gt_gt_cond(): 117 | result = [] 118 | for x in 10 >= x >= 5 if x%2==0 or x==5: 119 | result.append(x) 120 | assert result == [10, 8, 6, 5] 121 | 122 | def test_gt_gt_cond_paren(): 123 | result = [] 124 | for x in( 10 >= x >= 5 )if x%2==0 or x==5: 125 | result.append(x) 126 | assert result == [10, 8, 6, 5] 127 | 128 | 129 | if __name__ == "__main__": 130 | test_le_lt() 131 | test_le_lt_paren() 132 | test_le_lt_cond() 133 | test_le_le() 134 | test_le_le_cond() 135 | test_lt_le() 136 | test_lt_le_cond() 137 | test_lt_lt() 138 | test_lt_lt_cond() 139 | test_ge_gt() 140 | test_ge_gt_cond() 141 | test_ge_ge() 142 | test_ge_ge_cond() 143 | test_gt_ge() 144 | test_gt_ge_cond() 145 | test_gt_gt() 146 | test_gt_gt_cond() 147 | test_gt_gt_cond_paren() 148 | print("Success.") 149 | -------------------------------------------------------------------------------- /experimental/transformers/switch_statement.py: -------------------------------------------------------------------------------- 1 | '''from __experimental__ import switch_statement 2 | 3 | allows the use of a Pythonic switch statement (implemented with if clauses). 4 | A current limitation is that there can only be one level of switch statement 5 | i.e. you cannot have a switch statement inside a case of another switch statement. 6 | 7 | Here's an example usage 8 | 9 | def example(n): 10 | result = '' 11 | switch n: 12 | case 2: 13 | result += '2 is even and ' 14 | case 3, 5, 7: 15 | result += f'{n} is prime' 16 | break 17 | case 0: pass 18 | case 1: 19 | pass 20 | case 4, 6, 8, 9: 21 | result = f'{n} is not prime' 22 | break 23 | default: 24 | result = f'{n} is not a single digit integer' 25 | return result 26 | 27 | def test_switch(): 28 | assert example(0) == '0 is not prime' 29 | assert example(1) == '1 is not prime' 30 | assert example(2) == '2 is even and 2 is prime' 31 | assert example(3) == '3 is prime' 32 | assert example(4) == '4 is not prime' 33 | assert example(5) == '5 is prime' 34 | assert example(6) == '6 is not prime' 35 | assert example(7) == '7 is prime' 36 | assert example(8) == '8 is not prime' 37 | assert example(9) == '9 is not prime' 38 | assert example(10) == '10 is not a single digit integer' 39 | assert example(42) == '42 is not a single digit integer' 40 | ''' 41 | 42 | import builtins 43 | import tokenize 44 | from io import StringIO 45 | 46 | class Switch: 47 | ''' Adapted from http://code.activestate.com/recipes/410692/''' 48 | def __init__(self, value): 49 | self.value = value 50 | self.fall = False 51 | 52 | def __iter__(self): 53 | yield self.match 54 | raise StopIteration 55 | 56 | def __next__(self): 57 | """Return the match method once, then stop""" 58 | yield self.match 59 | raise StopIteration 60 | 61 | def match(self, *args): 62 | """Indicate whether or not to enter a case suite""" 63 | if self.fall or not args: 64 | return True 65 | elif self.value in args: # changed for v1.5, see below 66 | self.fall = True 67 | return True 68 | else: 69 | return False 70 | 71 | builtins._Switch = Switch 72 | 73 | def transform_source(text): 74 | '''Replaces instances of 75 | 76 | switch expression: 77 | by 78 | 79 | for __case in _Switch(n): 80 | 81 | and replaces 82 | 83 | case expression: 84 | 85 | by 86 | 87 | if __case(expression): 88 | 89 | and 90 | 91 | default: 92 | 93 | by 94 | 95 | if __case(): 96 | ''' 97 | toks = tokenize.generate_tokens(StringIO(text).readline) 98 | result = [] 99 | replacing_keyword = False 100 | for toktype, tokvalue, _, _, _ in toks: 101 | if toktype == tokenize.NAME and tokvalue == 'switch': 102 | result.extend([ 103 | (tokenize.NAME, 'for'), 104 | (tokenize.NAME, '__case'), 105 | (tokenize.NAME, 'in'), 106 | (tokenize.NAME, '_Switch'), 107 | (tokenize.OP, '(') 108 | ]) 109 | replacing_keyword = True 110 | elif toktype == tokenize.NAME and (tokvalue == 'case' or tokvalue == 'default'): 111 | result.extend([ 112 | (tokenize.NAME, 'if'), 113 | (tokenize.NAME, '__case'), 114 | (tokenize.OP, '(') 115 | ]) 116 | replacing_keyword = True 117 | elif replacing_keyword and tokvalue == ':': 118 | result.extend([ 119 | (tokenize.OP, ')'), 120 | (tokenize.OP, ':') 121 | ]) 122 | replacing_keyword = False 123 | else: 124 | result.append((toktype, tokvalue)) 125 | return tokenize.untokenize(result) 126 | 127 | -------------------------------------------------------------------------------- /experimental/core/import_hook.py: -------------------------------------------------------------------------------- 1 | #pylint: disable=W0603, W0122 2 | '''A custom Importer making use of the import hook capability 3 | 4 | Note that the protocole followed is no longer as described in PEP 302 [1] 5 | 6 | This code was adapted from 7 | http://stackoverflow.com/q/43571737/558799 8 | which is a question I asked when I wanted to adopt an approach using 9 | a deprecated module (imp) and which followed PEP 302. 10 | 11 | [1] https://www.python.org/dev/peps/pep-0302/ 12 | ''' 13 | 14 | import os.path 15 | import sys 16 | 17 | from importlib.abc import Loader, MetaPathFinder 18 | from importlib.util import spec_from_file_location 19 | 20 | from . import transforms 21 | 22 | # add the path where the default transformers should be found 23 | sys.path.append(os.path.abspath(os.path.join( 24 | os.path.dirname(__file__), "..", "transformers"))) 25 | 26 | MAIN_MODULE_NAME = None 27 | def import_main(name): 28 | '''Imports the module that is to be interpreted as the main module. 29 | 30 | experimental is often invoked with a script meant to be run as the 31 | main module its source is transformed. The invocation will be 32 | 33 | python -m experimental [trans1, trans2, ...] main_script 34 | 35 | Python identifies experimental as the main script; we artificially 36 | change this so that "main_script" is properly identified as such. 37 | ''' 38 | global MAIN_MODULE_NAME 39 | MAIN_MODULE_NAME = name 40 | return __import__(name) 41 | 42 | 43 | class MyMetaFinder(MetaPathFinder): 44 | '''A custom finder to locate modules. The main reason for this code 45 | is to ensure that our custom loader, which does the code transformations, 46 | is used.''' 47 | def find_spec(self, fullname, path, target=None): 48 | '''finds the appropriate properties (spec) of a module, and sets 49 | its loader.''' 50 | if not path: 51 | path = [os.getcwd()] 52 | if "." in fullname: 53 | name = fullname.split(".")[-1] 54 | else: 55 | name = fullname 56 | for entry in path: 57 | if os.path.isdir(os.path.join(entry, name)): 58 | # this module has child modules 59 | filename = os.path.join(entry, name, "__init__.py") 60 | submodule_locations = [os.path.join(entry, name)] 61 | else: 62 | filename = os.path.join(entry, name + ".py") 63 | submodule_locations = None 64 | if not os.path.exists(filename): 65 | continue 66 | 67 | return spec_from_file_location(fullname, filename, 68 | loader=MyLoader(filename), 69 | submodule_search_locations=submodule_locations) 70 | return None # we don't know how to import this 71 | 72 | sys.meta_path.insert(0, MyMetaFinder()) 73 | 74 | 75 | class MyLoader(Loader): 76 | '''A custom loader which will transform the source prior to its execution''' 77 | def __init__(self, filename): 78 | self.filename = filename 79 | 80 | def create_module(self, spec): 81 | return None # use default module creation semantics 82 | 83 | def exec_module(self, module): 84 | '''import the source code, transforma it before executing it so that 85 | it is known to Python.''' 86 | global MAIN_MODULE_NAME 87 | if module.__name__ == MAIN_MODULE_NAME: 88 | module.__name__ = "__main__" 89 | MAIN_MODULE_NAME = None 90 | 91 | with open(self.filename) as f: 92 | source = f.read() 93 | 94 | if transforms.transformers: 95 | source = transforms.transform(source) 96 | else: 97 | for line in source.split('\n'): 98 | if transforms.FROM_EXPERIMENTAL.match(line): 99 | ## transforms.transform will extract all such relevant 100 | ## lines and add them all relevant transformers 101 | source = transforms.transform(source) 102 | break 103 | exec(source, vars(module)) 104 | 105 | def get_code(self, _): 106 | '''Hack to silence an error when running experimental as main script 107 | See below for an explanation''' 108 | return compile("None", "", 'eval') 109 | 110 | """ 111 | When this code was run as part of a normal script, no error was raised. 112 | When I changed it into a package, and tried to run it as a module, an 113 | error occurred as shown below. By looking at the sources for the 114 | importlib module, I saw that some classes had a get_code() method which 115 | returned a code object. Rather than trying to recreate all the code, 116 | I wrote the above hack which seems to silence any error. 117 | 118 | $ python -m experimental 119 | Python version: 3.5.2 |Anaconda 4.2.0 (64-bit)| ... 120 | 121 | Python console with easily modifiable syntax. 122 | 123 | ~~> exit() 124 | Leaving non-standard console. 125 | 126 | Traceback (most recent call last): 127 | ... 128 | AttributeError: 'MyLoader' object has no attribute 'get_code' 129 | """ 130 | -------------------------------------------------------------------------------- /experimental/transformers/approx.py: -------------------------------------------------------------------------------- 1 | ''' 2 | from __experimental__ import approx 3 | 4 | defines some syntax for approximate comparisons within a certain tolerance that 5 | must have been previously defined by two variables visible in the current scope: 6 | 7 | rel_tol # relative tolerance 8 | abs_tol # absolute tolerance 9 | 10 | These comparisons are done using `math.isclose()`; see this function's 11 | docstring to learn more about the value of the two parameters. 12 | 13 | The comparison operators are: 14 | 15 | ~= # approximately equal 16 | <~= # less than or approximately equal 17 | >~= # greater than or approximately equal 18 | 19 | Given two mathematical terms or expressions a and b, they can occur: 20 | 21 | - on a single line 22 | - immediately following an assert keyword 23 | - immediately following an if keyword 24 | 25 | However, in the current implementation, anything else will fail. 26 | 27 | abs_tol = rel_tol = 1e-8 28 | assert 0.1 + 0.2 ~= 0.3 29 | 30 | will work; however 31 | 32 | abs_tol = rel_tol = 1e-8 33 | assert not 1 + 2 ~= 3 34 | 35 | will raise an AssertionError, because the `not` will not be parsed correctly. 36 | 37 | 38 | Here's the result of a quick demo 39 | 40 | > python -m experimental 41 | experimental console version 0.9.6. [Python version: 3.6.1] 42 | 43 | ~~> from __experimental__ import approx 44 | ~~> 0.1 + 0.2 45 | 0.30000000000000004 46 | ~~> 0.1 + 0.2 == 0.3 47 | False 48 | ~~> # Attempt to use approximate comparison without defining tolerances 49 | ~~> 0.1 + 0.2 ~= 0.3 50 | Traceback (most recent call last): 51 | File "", line 1, in 52 | NameError: name 'rel_tol' is not defined 53 | ~~> rel_tol = abs_tol = 1e-8 54 | ~~> 0.1 + 0.2 ~= 0.3 55 | True 56 | ~~> 2**0.5 ~= 1.414 57 | False 58 | ~~> abs_tol = 0.001 59 | ~~> 2**0.5 ~= 1.414 60 | True 61 | ''' 62 | 63 | import builtins 64 | 65 | def approx_eq(a, b, rel_tol, abs_tol): 66 | from math import isclose 67 | 68 | if abs_tol == rel_tol == 0: 69 | print("approximate comparisons are implemented using math.isclose") 70 | print("At least one of rel_tol or abs_tol must be defined either as a local or global variable.") 71 | raise NotImplementedError 72 | 73 | return isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol) 74 | 75 | def approx_le(a, b, rel_tol, abs_tol): 76 | return (a < b) or approx_eq(a, b, rel_tol, abs_tol) 77 | 78 | def approx_ge(a, b, rel_tol, abs_tol): 79 | return (a > b) or approx_eq(a, b, rel_tol, abs_tol) 80 | 81 | 82 | builtins.__approx_eq__ = approx_eq 83 | builtins.__approx_le__ = approx_le 84 | builtins.__approx_ge__ = approx_ge 85 | 86 | 87 | def transform_source(source): 88 | newlines = [] 89 | approx_present = False 90 | for line in source.splitlines(): 91 | line_without_comment = line.split('#')[0] 92 | for operator in ['<~=', '>~=', '~=', '≅']: 93 | if operator in line_without_comment: 94 | line = transform_line(line_without_comment, operator) 95 | break 96 | newlines.append(line) 97 | return '\n'.join(newlines) 98 | 99 | 100 | def transform_line(line, operator): 101 | assert line.find('#') == -1 102 | indent = ' '*(len(line) - len(line.lstrip())) 103 | line = line.lstrip() 104 | for keyword in ['assert ', 'if ']: 105 | if line.startswith(keyword): 106 | line = line[len(keyword):] 107 | break 108 | else: 109 | keyword = '' 110 | split = line.split(operator) 111 | lhs = split[0] 112 | rest = split[1] 113 | 114 | if ',' in rest: 115 | separator = ',' 116 | elif ':' in rest: 117 | separator = ':' 118 | else: 119 | separator = '' 120 | 121 | if separator: 122 | split = rest.split(separator) 123 | rhs = split[0] 124 | rest = split[1] 125 | else: 126 | rhs = rest 127 | rest = '' 128 | 129 | functions = { 130 | '~=': '__approx_eq__', 131 | '<~=': '__approx_le__', 132 | '>~=': '__approx_ge__' 133 | } 134 | 135 | new_line = (indent + keyword + functions[operator] + 136 | "(" + lhs + "," + rhs + ", rel_tol, abs_tol)" + 137 | separator + rest) 138 | return new_line 139 | 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Warning 2 | 3 | :warning: This project is kept for historical reason but it has rendered obsolete by the creation of [ideas](https://github.com/aroberge/ideas). 4 | 5 | 6 | ### A bit of nostalgia 7 | ```python 8 | > python -m experimental 9 | experimental console version 0.9.3 [Python version: 3.5.2] 10 | 11 | ~~> from __experimental__ import print_keyword 12 | ~~> print "Hello world!" 13 | Hello world! 14 | ``` 15 | 16 | # What is `experimental`? 17 | 18 | `experimental` is a simple Python module intended to facilitate exploring different syntax construct in Python in an easy way. Unless you have a very compelling and almost unimaginable reason to do so, 19 | :warning: **it should not be used in production**. 20 | 21 | Without `experimental`, if you want to modify Python's syntax, say by adding a new keyword, you need to: 22 | 23 | 1. Get a copy of Python's repository on your computer 24 | 2. modify the grammar file 25 | 3. modify the lexer 26 | 4. modify the parser 27 | 5. modify the compiler 28 | 6. recompile all the sources 29 | 30 | This is a very involved process. 31 | `experimental` is a Python module that provides a much simpler way to experiment with changes to Python's syntax. 32 | 33 | ## Installation 34 | 35 | To install `experimental`, you can use the standard way: 36 | 37 | pip install experimental 38 | 39 | `experimental` currently requires Python 3.4+. 40 | 41 | ## Usage overview 42 | 43 | There are many ways to use `experimental`. 44 | 45 | ### Alternative Python console 46 | If you simply want to have start a experimental Python console, as shown at the top of this readme file, type 47 | 48 | python -m experimental 49 | 50 | 51 | ### Automatically processing a file - 1 52 | 53 | Suppose you have the following file: 54 | 55 | ```python 56 | > type test.py 57 | from __experimental__ import print_keyword 58 | print "Hello world!" 59 | ``` 60 | 61 | Simply add the name of the test file (without the .py extension) at the end. 62 | 63 | ``` 64 | > python -m experimental test 65 | Hello world! 66 | ``` 67 | 68 | ### Automatically processing a file - 2 69 | 70 | You can also activate some transformations by inserting them on the 71 | command line between `experimental` 72 | and the name of your python script on the command line. 73 | 74 | ```python 75 | > type test.py 76 | print "Hello world!" 77 | 78 | > python -m experimental print_keyword test 79 | Hello world! 80 | ``` 81 | 82 | ### Automatically processing a file and activating a console 83 | 84 | Like normal Python, you can execute a script and start an interactive session 85 | afterwards by using the `-i` flag 86 | 87 | ```python 88 | > type test.py 89 | print "Hello world!" 90 | my_variable = 3 91 | print 92 | 93 | > python -i -m experimental print_keyword test 94 | Hello world! 95 | 96 | experimental console version 0.9.3 [Python version: 3.5.2] 97 | 98 | ~~> my_variable 99 | 3 100 | ``` 101 | 102 | ### Everything but the kitchen sink approach 103 | 104 | You can combine declarations within a file with declarations on the command line. 105 | 106 | ```python 107 | > type test.py 108 | from __experimental__ import increment, decrement 109 | from __experimental__ import nobreak_keyword 110 | from __experimental__ import int_seq 111 | 112 | square = function x: x**2 113 | 114 | my_variable = 6 115 | 116 | for i in 4 < i <= 7 if my_variable==i: 117 | my_variable++ 118 | nobreak: 119 | my_variable = square(my_variable) 120 | 121 | print my_variable 122 | ``` 123 | 124 | ```python 125 | > python -i -m experimental print_keyword function_keyword test 126 | 49 127 | experimental console version 0.9.3. [Python version: 3.5.2] 128 | 129 | ~~> my_variable-- 130 | ~~> print my_variable 131 | 48 132 | ~~> from __experimental__ import repeat_keyword 133 | ~~> repeat 3: 134 | ... print "This is definitely **not** Python." 135 | ... 136 | This is definitely **not** Python. 137 | This is definitely **not** Python. 138 | This is definitely **not** Python. 139 | ~~> 140 | ``` 141 | 142 | ## Additional information 143 | 144 | ### Dependencies 145 | 146 | `experimental` only uses code from the standard library for its execution. However, for testing, I most often use [pytest](https://docs.pytest.org/en/latest/contents.html) to collect and run all the tests, which are simple assertion based comparisons. 147 | 148 | ### How does it work? 149 | 150 | `experimental` uses an import hook to replace the usual import mechanism. Normally, a Python file is first located, then its source is read and finally it is executed _as is_. With `experimental`, an extra step is inserted after the file is read so that its source code can be modified in memory prior to being executed. 151 | 152 | ### Available transformations 153 | 154 | See [the readme file in the transformers directory](https://github.com/aroberge/experimental/blob/master/experimental/transformers/readme.md). Some transformations are **robust**, whereas others ... well, you are very likely to find situations where some transformations are not behaving as you'd expect. These are not bugs, you understand, but rather invitations for you to explore the poorly written code, make some improvements and submit them for consideration. 155 | 156 | You are also more than welcome to submit your own experimental code transformations. I'm particularly interested to see new approaches used to transform source code. If you do so, you should include at least some minimal examples as test cases. 157 | 158 | ### Limitation of the console 159 | 160 | Code transformations done in the console are performed on a "line by line" basis. 161 | As a result, transformations that work with an entire code block are likely to fail 162 | in the console. An example is the `where_clause` 163 | If you create similar transformations, you might want to define a global 164 | variable `NO_CONSOLE` in your module, as was done in 165 | [where_clause](https://github.com/aroberge/experimental/blob/master/experimental/transformers/where_clause.py). 166 | 167 | 168 | ### Automated tests 169 | 170 | See [the readme file in the tests directory](https://github.com/aroberge/experimental/blob/master/tests/readme.md) for details. 171 | 172 | 173 | ## To do 174 | 175 | - Add version based on `imp` for older Python versions. 176 | 177 | -------------------------------------------------------------------------------- /experimental/core/transforms.py: -------------------------------------------------------------------------------- 1 | #pylint: disable=W1401, C0103, W0703 2 | '''This module takes care of identifying, importing and adding source 3 | code transformers. It also contains a function, `transform`, which 4 | takes care of invoking all known transformers to convert a source code. 5 | ''' 6 | import re 7 | import sys 8 | 9 | FROM_EXPERIMENTAL = re.compile("(^from\s+__experimental__\s+import\s+)") 10 | CONSOLE_ACTIVE = False # changed by console.start_console() 11 | 12 | class NullTransformer: 13 | '''NullTransformer is a convenience class which can generate instances 14 | to be used when a given transformer cannot be imported.''' 15 | def transform_source(self, source): #pylint: disable=I0011, R0201, C0111 16 | return source 17 | 18 | transformers = {} 19 | def add_transformers(line): 20 | '''Extract the transformers names from a line of code of the form 21 | from __experimental__ import transformer1 [,...] 22 | and adds them to the globally known dict 23 | ''' 24 | assert FROM_EXPERIMENTAL.match(line) 25 | 26 | line = FROM_EXPERIMENTAL.sub(' ', line) 27 | # we now have: " transformer1 [,...]" 28 | line = line.split("#")[0] # remove any end of line comments 29 | # and insert each transformer as an item in a list 30 | for trans in line.replace(' ', '').split(','): 31 | import_transformer(trans) 32 | 33 | 34 | def import_transformer(name): 35 | '''If needed, import a transformer, and adds it to the globally known dict 36 | The code inside a module where a transformer is defined should be 37 | standard Python code, which does not need any transformation. 38 | So, we disable the import hook, and let the normal module import 39 | do its job - which is faster and likely more reliable than our 40 | custom method. 41 | ''' 42 | if name in transformers: 43 | return transformers[name] 44 | 45 | # We are adding a transformer built from normal/standard Python code. 46 | # As we are not performing transformations, we temporarily disable 47 | # our import hook, both to avoid potential problems AND because we 48 | # found that this resulted in much faster code. 49 | hook = sys.meta_path[0] 50 | sys.meta_path = sys.meta_path[1:] 51 | try: 52 | transformers[name] = __import__(name) 53 | # Some transformers are not allowed in the console. 54 | # If an attempt is made to activate one of them in the console, 55 | # we replace it by a transformer that does nothing and print a 56 | # message specific to that transformer as written in its module. 57 | if CONSOLE_ACTIVE: 58 | if hasattr(transformers[name], "NO_CONSOLE"): 59 | print(transformers[name].NO_CONSOLE) 60 | transformers[name] = NullTransformer() 61 | except ImportError: 62 | sys.stderr.write("Warning: Import Error in add_transformers: %s not found\n" % name) 63 | transformers[name] = NullTransformer() 64 | except Exception as e: 65 | sys.stderr.write("Unexpected exception in transforms.import_transformer%s\n " % 66 | e.__class__.__name__) 67 | finally: 68 | sys.meta_path.insert(0, hook) # restore import hook 69 | 70 | return transformers[name] 71 | 72 | def extract_transformers_from_source(source): 73 | '''Scan a source for lines of the form 74 | from __experimental__ import transformer1 [,...] 75 | identifying transformers to be used. Such line is passed to the 76 | add_transformer function, after which it is removed from the 77 | code to be executed. 78 | ''' 79 | lines = source.split('\n') 80 | linenumbers = [] 81 | for number, line in enumerate(lines): 82 | if FROM_EXPERIMENTAL.match(line): 83 | add_transformers(line) 84 | linenumbers.insert(0, number) 85 | 86 | # drop the "fake" import from the source code 87 | for number in linenumbers: 88 | del lines[number] 89 | return '\n'.join(lines) 90 | 91 | def remove_not_allowed_in_console(): 92 | '''This function should be called from the console, when it starts. 93 | 94 | Some transformers are not allowed in the console and they could have 95 | been loaded prior to the console being activated. We effectively remove them 96 | and print an information message specific to that transformer 97 | as written in the transformer module. 98 | 99 | ''' 100 | not_allowed_in_console = [] 101 | if CONSOLE_ACTIVE: 102 | for name in transformers: 103 | tr_module = import_transformer(name) 104 | if hasattr(tr_module, "NO_CONSOLE"): 105 | not_allowed_in_console.append((name, tr_module)) 106 | for name, tr_module in not_allowed_in_console: 107 | print(tr_module.NO_CONSOLE) 108 | # Note: we do not remove them, so as to avoid seeing the 109 | # information message displayed again if an attempt is 110 | # made to re-import them from a console instruction. 111 | transformers[name] = NullTransformer() 112 | 113 | 114 | def transform(source): 115 | '''Used to convert the source code, making use of known transformers. 116 | 117 | "transformers" are modules which must contain a function 118 | 119 | transform_source(source) 120 | 121 | which returns a tranformed source. 122 | Some transformers (for example, those found in the standard library 123 | module lib2to3) cannot cope with non-standard syntax; as a result, they 124 | may fail during a first attempt. We keep track of all failing 125 | transformers and keep retrying them until either they all succeeded 126 | or a fixed set of them fails twice in a row. 127 | ''' 128 | source = extract_transformers_from_source(source) 129 | 130 | # Some transformer fail when multiple non-Python constructs 131 | # are present. So, we loop multiple times keeping track of 132 | # which transformations have been unsuccessfully performed. 133 | not_done = transformers 134 | while True: 135 | failed = {} 136 | for name in not_done: 137 | tr_module = import_transformer(name) 138 | try: 139 | source = tr_module.transform_source(source) 140 | except Exception as e: 141 | failed[name] = tr_module 142 | # from traceback import print_exc 143 | # print("Unexpected exception in transforms.transform", 144 | # e.__class__.__name__) 145 | # print_exc() 146 | 147 | if not failed: 148 | break 149 | # Insanity is doing the same Tting over and overaAgain and 150 | # expecting different results ... 151 | # If the exact same set of transformations are not performed 152 | # twice in a row, there is no point in trying out a third time. 153 | if failed == not_done: 154 | print("Warning: the following transforms could not be done:") 155 | for key in failed: 156 | print(key) 157 | break 158 | not_done = failed # attempt another pass 159 | 160 | return source 161 | -------------------------------------------------------------------------------- /experimental/transformers/int_seq.py: -------------------------------------------------------------------------------- 1 | ''' from __experimental__ import int_seq 2 | 3 | makes it possible to use an alternative syntax instead of using `range` 4 | in a for loop. To be more specific, instead of 5 | 6 | for i in range(3): 7 | print(i) 8 | 9 | we could write 10 | 11 | for i in 0 <= i < 3: 12 | print(i) 13 | 14 | or 15 | 16 | for i in 0 <= i <= 2: # compare upper boundary with previous case 17 | print(i) 18 | 19 | By reversing the order of the comparison operators, we iterate in reverse. 20 | Thus, for example 21 | 22 | for i in 10 >= i > 0: 23 | print(i) 24 | 25 | would be equivalent to 26 | 27 | for i in range(10, 0, -1): 28 | print(i) 29 | 30 | An additional condition can be added; for example 31 | 32 | for i in 1 <= i < 10 if (i % 2 == 0): 33 | print(i) 34 | 35 | would print the first 4 even integers. 36 | 37 | In addition, `inseq` is possible to use as a keyword instead of `in`. 38 | `inseq` is meant to mean `in sequence`. Also, the "range" can be enclosed 39 | in parentheses for greater clarity. Thus, the following is valid: 40 | 41 | for i inseq (1 <= i < 10) if (i % 2 == 0): 42 | print(i) 43 | 44 | The transformation is done using a regex search and is only valid 45 | on a single line. **There is no guarantee that all legitimately 46 | valid cases will be recognized as such.** 47 | ''' 48 | import builtins 49 | import re 50 | 51 | def __experimental_range(start, stop, var, cond, loc={}): 52 | '''Utility function made to reproduce range() with unit integer step 53 | but with the added possibility of specifying a condition 54 | on the looping variable (e.g. var % 2 == 0) 55 | ''' 56 | locals().update(loc) 57 | if start < stop: 58 | for __ in range(start, stop): 59 | locals()[var] = __ 60 | if eval(cond, globals(), locals()): 61 | yield __ 62 | else: 63 | for __ in range(start, stop, -1): 64 | locals()[var] = __ 65 | if eval(cond, globals(), locals()): 66 | yield __ 67 | 68 | builtins.__experimental_range = __experimental_range 69 | 70 | ###################################################################### 71 | # 72 | # WARNING 73 | # 74 | # In the unlikely case that you know less about regular expressions than I do 75 | # please do not use what I do as any indication of how one should use regular 76 | # expressions (regex). 77 | # 78 | # The regex use below is admitedly awful, very likely sub-optimal, 79 | # and could almost certainly be vastly improved upon, either by someone 80 | # who actually knows how to use regular expressions effectively or, 81 | # even better, by not using regular expressions at all, and either using 82 | # Python's tokenize module, or writing a custom parser. 83 | # 84 | ####################################################################### 85 | 86 | no_condition = r"""(?P\s*for\s+) 87 | (?P[a-zA-Z_]\w*) 88 | \s+ (in|inseq) \s* 89 | \(?\s* # optional opening ( 90 | (?P[-\w]+) 91 | \s* %s \s* 92 | (?P=var) 93 | \s* %s \s* 94 | (?P[-\w]+) 95 | \s*\)? # optional closing ) 96 | \s* : \s* 97 | """ 98 | # A problem with the optional () is that the regex will be 99 | # satisfied if only one of them is present. We'll take care of 100 | # this by ensuring an equal number of opening and closing parentheses. 101 | 102 | cases = [] 103 | le_lt = re.compile(no_condition % ("<=", "<"), re.VERBOSE) 104 | cases.append((le_lt, "{0} {1} in range({2}, {3}):")) 105 | 106 | le_le = re.compile(no_condition % ("<=", "<="), re.VERBOSE) 107 | cases.append((le_le, "{0} {1} in range({2}, {3}+1):")) 108 | 109 | lt_lt = re.compile(no_condition % ("<", "<"), re.VERBOSE) 110 | cases.append((lt_lt, "{0} {1} in range({2}+1, {3}):")) 111 | 112 | lt_le = re.compile(no_condition % ("<", "<="), re.VERBOSE) 113 | cases.append((lt_le, "{0} {1} in range({2}+1, {3}+1):")) 114 | 115 | ge_gt = re.compile(no_condition % (">=", ">"), re.VERBOSE) 116 | cases.append((ge_gt, "{0} {1} in range({2}, {3}, -1):")) 117 | 118 | ge_ge = re.compile(no_condition % (">=", ">="), re.VERBOSE) 119 | cases.append((ge_ge, "{0} {1} in range({2}, {3}-1, -1):")) 120 | 121 | gt_gt = re.compile(no_condition % (">", ">"), re.VERBOSE) 122 | cases.append((gt_gt, "{0} {1} in range({2}-1, {3}, -1):")) 123 | 124 | gt_ge = re.compile(no_condition % (">", ">="), re.VERBOSE) 125 | cases.append((gt_ge, "{0} {1} in range({2}-1, {3}-1, -1):")) 126 | 127 | with_condition = r"""(?P\s*for\s+) 128 | (?P[a-zA-Z_]\w*) 129 | \s+ (in|inseq) \s* 130 | \(?\s* # optional opening ( 131 | (?P[-\w]+) 132 | \s* %s \s* 133 | (?P=var) 134 | \s* %s \s* 135 | (?P[-\w]+) 136 | \s*\)? # optional closing ) 137 | \s* if \s+ 138 | (?P.+) 139 | \s* : \s* 140 | """ 141 | le_lt_cond = re.compile(with_condition % ("<=", "<"), re.VERBOSE) 142 | cases.append((le_lt_cond, "{0} {1} in __experimental_range({2}, {3}, '{1}', '{4}', loc=locals()):")) 143 | 144 | le_le_cond = re.compile(with_condition % ("<=", "<="), re.VERBOSE) 145 | cases.append((le_le_cond, "{0} {1} in __experimental_range({2}, {3}+1, '{1}', '{4}', loc=locals()):")) 146 | 147 | lt_lt_cond = re.compile(with_condition % ("<", "<"), re.VERBOSE) 148 | cases.append((lt_lt_cond, "{0} {1} in __experimental_range({2}+1, {3}, '{1}', '{4}', loc=locals()):")) 149 | 150 | lt_le_cond = re.compile(with_condition % ("<", "<="), re.VERBOSE) 151 | cases.append((lt_le_cond, "{0} {1} in __experimental_range({2}+1, {3}+1, '{1}', '{4}', loc=locals()):")) 152 | 153 | ge_gt_cond = re.compile(with_condition % (">=", ">"), re.VERBOSE) 154 | cases.append((ge_gt_cond, "{0} {1} in __experimental_range({2}, {3}, '{1}', '{4}', loc=locals()):")) 155 | 156 | ge_ge_cond = re.compile(with_condition % (">=", ">="), re.VERBOSE) 157 | cases.append((ge_ge_cond, "{0} {1} in __experimental_range({2}, {3}-1, '{1}', '{4}', loc=locals()):")) 158 | 159 | gt_gt_cond = re.compile(with_condition % (">", ">"), re.VERBOSE) 160 | cases.append((gt_gt_cond, "{0} {1} in __experimental_range({2}-1, {3}, '{1}', '{4}', loc=locals()):")) 161 | 162 | gt_ge_cond = re.compile(with_condition % (">", ">="), re.VERBOSE) 163 | cases.append((gt_ge_cond, "{0} {1} in __experimental_range({2}-1, {3}-1, '{1}', '{4}', loc=locals()):")) 164 | 165 | 166 | def transform_source(source): 167 | lines = source.split("\n") 168 | new_lines = [] 169 | for line in lines: 170 | begin = line.split("#")[0] 171 | for (pattern, for_str) in cases: 172 | result = pattern.search(begin) 173 | if result is not None and begin.count('(') == begin.count(')'): 174 | line = create_for(for_str, result) 175 | break 176 | new_lines.append(line) 177 | return "\n".join(new_lines) 178 | 179 | 180 | def create_for(line, search_result): 181 | '''Create a new "for loop" line as a replacement for the original code. 182 | ''' 183 | try: 184 | return line.format(search_result.group("indented_for"), 185 | search_result.group("var"), 186 | search_result.group("start"), 187 | search_result.group("stop"), 188 | search_result.group("cond")) 189 | except IndexError: 190 | return line.format(search_result.group("indented_for"), 191 | search_result.group("var"), 192 | search_result.group("start"), 193 | search_result.group("stop")) 194 | -------------------------------------------------------------------------------- /experimental/transformers/readme.md: -------------------------------------------------------------------------------- 1 | 2 | Most of the content of this readme has been automatically extracted from 3 | the docstring of each file found in this directory. 4 | 5 | Note that multiple transforms can be used in a single file, e.g. 6 | 7 | ```python 8 | from __experimental__ import increment, decrement 9 | from __experimental__ import function_keyword 10 | ``` 11 | 12 | 13 | ## approx.py 14 | 15 | 16 | from __experimental__ import approx 17 | 18 | defines some syntax for approximate comparisons within a certain tolerance that 19 | must have been previously defined by two variables visible in the current scope: 20 | 21 | rel_tol # relative tolerance 22 | abs_tol # absolute tolerance 23 | 24 | These comparisons are done using `math.isclose()`; see this function's 25 | docstring to learn more about the value of the two parameters. 26 | 27 | The comparison operators are: 28 | 29 | ~= # approximately equal 30 | <~= # less than or approximately equal 31 | >~= # greater than or approximately equal 32 | 33 | Given two mathematical terms or expressions a and b, they can occur: 34 | 35 | - on a single line 36 | - immediately following an assert keyword 37 | - immediately following an if keyword 38 | 39 | However, in the current implementation, anything else will fail. 40 | 41 | abs_tol = rel_tol = 1e-8 42 | assert 0.1 + 0.2 ~= 0.3 43 | 44 | will work; however 45 | 46 | abs_tol = rel_tol = 1e-8 47 | assert not 1 + 2 ~= 3 48 | 49 | will raise an AssertionError, because the `not` will not be parsed correctly. 50 | 51 | 52 | Here's the result of a quick demo 53 | 54 | > python -m experimental 55 | experimental console version 0.9.6. [Python version: 3.6.1] 56 | 57 | ~~> from __experimental__ import approx 58 | ~~> 0.1 + 0.2 59 | 0.30000000000000004 60 | ~~> 0.1 + 0.2 == 0.3 61 | False 62 | ~~> # Attempt to use approximate comparison without defining tolerances 63 | ~~> 0.1 + 0.2 ~= 0.3 64 | Traceback (most recent call last): 65 | File "", line 1, in 66 | NameError: name 'rel_tol' is not defined 67 | ~~> rel_tol = abs_tol = 1e-8 68 | ~~> 0.1 + 0.2 ~= 0.3 69 | True 70 | ~~> 2**0.5 ~= 1.414 71 | False 72 | ~~> abs_tol = 0.001 73 | ~~> 2**0.5 ~= 1.414 74 | True 75 | 76 | 77 | ## convert_py2.py 78 | 79 | from __experimental__ import convert_py2 80 | 81 | triggers the use of the lib2to3 Python library to automatically convert 82 | the code from Python 2 to Python 3 prior to executing it. 83 | 84 | As long as lib2to3 can convert the code, this means that code written 85 | using Python 2 syntax can be run using a Python 3 interpreter. 86 | 87 | 88 | ## decrement.py 89 | 90 | 91 | from __experimental__ import decrement 92 | 93 | enables transformation of code of the form 94 | 95 | name -- # optional comment 96 | other-- 97 | 98 | into 99 | 100 | name -= 1 # optional comment 101 | other-= 1 102 | 103 | Space(s) betwen `name` and `--` are ignored. 104 | 105 | This change is done as a simple string replacement, on a line by line basis. 106 | Therefore, it can change not only code but content of triple quoted strings 107 | as well. A more robust solution could always be implemented 108 | using the tokenize module. 109 | 110 | 111 | ## french_syntax.py 112 | 113 | from __experimental__ import french_syntax 114 | 115 | allows the use of a predefined subset of Python keyword to be written 116 | as their French equivalent; **English and French keywords can be mixed**. 117 | 118 | Thus, code like: 119 | 120 | si Vrai: 121 | imprime("French can be used.") 122 | autrement: 123 | print(Faux) 124 | 125 | Will be translated to 126 | 127 | if True: 128 | print("French can be used.") 129 | else: 130 | print(False) 131 | 132 | This type of transformation could be useful when teaching the 133 | very basic concepts of programming to (young) beginners who use 134 | non-ascii based language and would find it difficult to type 135 | ascii characters. 136 | 137 | The transformation is done using the tokenize module; it should 138 | only affect code and not content of strings. 139 | 140 | 141 | ## function_keyword.py 142 | 143 | from __experimental__ import function_keyword 144 | 145 | enables to use the word `function` instead of `lambda`, as in 146 | 147 | square = function x: x**2 148 | 149 | square(3) # returns 9 150 | 151 | `lambda` can still be used in the source code. 152 | 153 | The transformation is done using the tokenize module; it should 154 | only affect code and not content of strings. 155 | 156 | 157 | ## increment.py 158 | 159 | 160 | from __experimental__ import increment 161 | 162 | enables transformation of code of the form 163 | 164 | name ++ # optional comment 165 | other++ 166 | 167 | into 168 | 169 | name += 1 # optional comment 170 | other+= 1 171 | 172 | Space(s) betwen `name` and `++` are ignored. 173 | 174 | This change is done as a simple string replacement, on a line by line basis. 175 | Therefore, it can change not only code but content of triple quoted strings 176 | as well. A more robust solution could always be implemented 177 | using the tokenize module. 178 | 179 | 180 | ## int_seq.py 181 | 182 | from __experimental__ import int_seq 183 | 184 | makes it possible to use an alternative syntax instead of using `range` 185 | in a for loop. To be more specific, instead of 186 | 187 | for i in range(3): 188 | print(i) 189 | 190 | we could write 191 | 192 | for i in 0 <= i < 3: 193 | print(i) 194 | 195 | or 196 | 197 | for i in 0 <= i <= 2: # compare upper boundary with previous case 198 | print(i) 199 | 200 | By reversing the order of the comparison operators, we iterate in reverse. 201 | Thus, for example 202 | 203 | for i in 10 >= i > 0: 204 | print(i) 205 | 206 | would be equivalent to 207 | 208 | for i in range(10, 0, -1): 209 | print(i) 210 | 211 | An additional condition can be added; for example 212 | 213 | for i in 1 <= i < 10 if (i % 2 == 0): 214 | print(i) 215 | 216 | would print the first 4 even integers. 217 | 218 | In addition, `inseq` is possible to use as a keyword instead of `in`. 219 | `inseq` is meant to mean `in sequence`. Also, the "range" can be enclosed 220 | in parentheses for greater clarity. Thus, the following is valid: 221 | 222 | for i inseq (1 <= i < 10) if (i % 2 == 0): 223 | print(i) 224 | 225 | The transformation is done using a regex search and is only valid 226 | on a single line. **There is no guarantee that all legitimately 227 | valid cases will be recognized as such.** 228 | 229 | 230 | ## nobreak_keyword.py 231 | 232 | from __experimental__ import nobreak_keyword 233 | 234 | enables to use the fake keyword `nobreak` instead of `else`, as in 235 | 236 | for i in range(3): 237 | print(i) 238 | nobreak: 239 | print("The entire loop was run.") 240 | 241 | Note that `nobreak` can be use everywhere `else` could be used, 242 | (including in `if` blocks) even if would not make sense. 243 | 244 | The transformation is done using the tokenize module; it should 245 | only affect code and not content of strings. 246 | 247 | 248 | ## pep542.py 249 | 250 | from __experimental__ import pep542 251 | 252 | Trying to implement https://www.python.org/dev/peps/pep-0542/ 253 | 254 | 255 | ## print_keyword.py 256 | 257 | from __experimental__ import print_keyword 258 | 259 | triggers the use of the lib2to3 Python library to automatically convert 260 | all `print` statements (assumed to use the Python 2 syntax) into 261 | function calls. 262 | 263 | 264 | ## repeat_keyword.py 265 | 266 | from __experimental__ import repeat_keyword 267 | 268 | introduces `repeat` as a keyword to write simple loops that repeat 269 | a set number of times. That is: 270 | 271 | repeat 3: 272 | a = 2 273 | repeat a*a: 274 | pass 275 | 276 | is equivalent to 277 | 278 | for __VAR_1 in range(3): 279 | a = 2 280 | for __VAR_2 in range(a*a): 281 | pass 282 | 283 | The names of the variables are chosen so as to ensure that they 284 | do not appear in the source code to be translated. 285 | 286 | The transformation is done using the tokenize module; it should 287 | only affect code and not content of strings. 288 | 289 | 290 | ## spanish_syntax.py 291 | 292 | from __experimental__ import spanish_syntax 293 | 294 | allows the use of a predefined subset of Python keyword to be written 295 | as their Spanish equivalent; **English and Spanish keywords can be mixed**. 296 | 297 | Neutral latin-american Spanish 298 | - translation by Sebastian Silva 299 | 300 | Thus, code like: 301 | 302 | si Verdadero: 303 | imprime("Spanish can be used.") 304 | sino: 305 | print(Falso) 306 | 307 | Will be translated to 308 | 309 | if True: 310 | print("Spanish can be used.") 311 | else: 312 | print(False) 313 | 314 | This type of transformation could be useful when teaching the 315 | very basic concepts of programming to (young) beginners who use 316 | non-ascii based language and would find it difficult to type 317 | ascii characters. 318 | 319 | The transformation is done using the tokenize module; it should 320 | only affect code and not content of strings. 321 | 322 | 323 | ## switch_statement.py 324 | 325 | from __experimental__ import switch_statement 326 | 327 | allows the use of a Pythonic switch statement (implemented with if clauses). 328 | A current limitation is that there can only be one level of switch statement 329 | i.e. you cannot have a switch statement inside a case of another switch statement. 330 | 331 | Here's an example usage 332 | 333 | def example(n): 334 | result = '' 335 | switch n: 336 | case 2: 337 | result += '2 is even and ' 338 | case 3, 5, 7: 339 | result += f'{n} is prime' 340 | break 341 | case 0: pass 342 | case 1: 343 | pass 344 | case 4, 6, 8, 9: 345 | result = f'{n} is not prime' 346 | break 347 | default: 348 | result = f'{n} is not a single digit integer' 349 | return result 350 | 351 | def test_switch(): 352 | assert example(0) == '0 is not prime' 353 | assert example(1) == '1 is not prime' 354 | assert example(2) == '2 is even and 2 is prime' 355 | assert example(3) == '3 is prime' 356 | assert example(4) == '4 is not prime' 357 | assert example(5) == '5 is prime' 358 | assert example(6) == '6 is not prime' 359 | assert example(7) == '7 is prime' 360 | assert example(8) == '8 is not prime' 361 | assert example(9) == '9 is not prime' 362 | assert example(10) == '10 is not a single digit integer' 363 | assert example(42) == '42 is not a single digit integer' 364 | 365 | 366 | ## where_clause.py 367 | 368 | from __experimental__ import where_clause 369 | 370 | shows how one could use `where` as a keyword to introduce a code 371 | block that would be ignored by Python. The idea was to use this as 372 | a _pythonic_ notation as an alternative for the optional type hinting described 373 | in PEP484. **This idea has been rejected** as it would not have 374 | been compatible with some older versions of Python, unlike the 375 | approach that has been accepted. 376 | https://www.python.org/dev/peps/pep-0484/#other-forms-of-new-syntax 377 | 378 | :warning: This transformation **cannot** be used in the console. 379 | 380 | For more details, please see two of my recent blog posts: 381 | 382 | https://aroberge.blogspot.ca/2015/12/revisiting-old-friend-yet-again.html 383 | 384 | https://aroberge.blogspot.ca/2015/01/type-hinting-in-python-focus-on.html 385 | 386 | I first suggested this idea more than 12 years ago! ;-) 387 | 388 | https://aroberge.blogspot.ca/2005/01/where-keyword-and-python-as-pseudo.html 389 | 390 | 391 | --------------------------------------------------------------------------------