├── tests ├── __init__.py ├── test_stream.py ├── test_iter.py ├── test_pipe.py └── test_infix.py ├── recipes ├── echo.py ├── mapred.py └── cat.py ├── syntax_sugar ├── __init__.py ├── _infix.py ├── _placeholder.py ├── _composable.py ├── _match.py ├── _util.py ├── _stream.py ├── _iter.py └── _pipe.py ├── CONTRIBUTING.md ├── setup.py ├── .travis.yml ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /recipes/echo.py: -------------------------------------------------------------------------------- 1 | from syntax_sugar import * 2 | from sys import argv 3 | 4 | pipe(argv[1:]) | ' '.join | print | END 5 | -------------------------------------------------------------------------------- /recipes/mapred.py: -------------------------------------------------------------------------------- 1 | from syntax_sugar import * 2 | from functools import reduce 3 | 4 | (pipe(10) 5 | | range 6 | | (map, lambda x: x ** 2) 7 | | sum 8 | | print 9 | | END) 10 | -------------------------------------------------------------------------------- /recipes/cat.py: -------------------------------------------------------------------------------- 1 | from syntax_sugar import * 2 | from sys import argv 3 | from functools import partial 4 | 5 | 6 | pipe(argv[1:]) | each(lambda filename: open(filename).read()) | ''.join | partial(print, end='') | END 7 | -------------------------------------------------------------------------------- /syntax_sugar/__init__.py: -------------------------------------------------------------------------------- 1 | from ._util import * 2 | from ._composable import * 3 | from ._pipe import * 4 | from ._infix import * 5 | from ._stream import * 6 | from ._placeholder import * 7 | from ._match import * 8 | from ._iter import * 9 | -------------------------------------------------------------------------------- /syntax_sugar/_infix.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | __all__ = [ 4 | 'infix', 5 | ] 6 | 7 | class infix(partial): 8 | def __truediv__(self, right): 9 | return self(right) 10 | 11 | def __rtruediv__(self, left): 12 | return infix(self.func, left) 13 | -------------------------------------------------------------------------------- /syntax_sugar/_placeholder.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'placeholder', 3 | '_', 4 | ] 5 | 6 | class PlaceHolder: 7 | def __getattribute__(self, action): 8 | def wrapper(*argv, **kwargv): 9 | return lambda data: getattr(data, action)(*argv, **kwargv) 10 | return wrapper 11 | 12 | _ = placeholder = PlaceHolder() 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTION 2 | 3 | Thank you for being interested in contributing to this project. 4 | 5 | # Run test suites 6 | 7 | Clone this repository 8 | 9 | ``` 10 | git clone https://github.com/czheo/syntax_sugar_python 11 | ``` 12 | 13 | Install syntax_sugar as an editable package 14 | 15 | ``` 16 | cd syntax_sugar_python 17 | pip install -e . 18 | ``` 19 | 20 | Run tests 21 | 22 | ``` 23 | python setup.py pytest 24 | ``` -------------------------------------------------------------------------------- /tests/test_stream.py: -------------------------------------------------------------------------------- 1 | from syntax_sugar import * 2 | 3 | def test_stream_collection(): 4 | assert list(stream() << [1,2,3] << [4,5,6]) == list([1,2,3,4,5,6]) 5 | assert list(stream() << [1,2,3] << range(4,7)) == list([1,2,3,4,5,6]) 6 | 7 | def test_stream_function(): 8 | assert list((stream() << [1,2,3] << (lambda x: x+1)) /take/ 5) == list([1,2,3,4,5]) 9 | assert list((stream() << [1,2,3] << (lambda x: x+1)) /drop/ 3 /take/ 5) == list([4,5,6,7,8]) 10 | assert list((stream() << range(1,10) << (lambda x: x+1)) /drop/ 3 /take/ 5) == list([4,5,6,7,8]) 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="syntax_sugar", 5 | version="0.3.7", 6 | url='https://github.com/czheo/syntax_sugar_python', 7 | description="add syntactic sugar to Python", 8 | author="czheo", 9 | license="MIT", 10 | keywords="syntax, functional", 11 | packages=find_packages(), 12 | install_requires=[ 13 | 'multiprocess', 14 | 'eventlet', 15 | ], 16 | setup_requires=[ 17 | 'pytest-runner', 18 | ], 19 | tests_require=[ 20 | 'pytest', 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /syntax_sugar/_composable.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from functools import reduce 3 | from ._infix import infix 4 | 5 | 6 | __all__ = [ 7 | 'composable', 8 | 'compose', 9 | 'o', 10 | ] 11 | 12 | class composable(partial): 13 | def __mul__(self, rhs): 14 | return lambda *args, **kwargs: self(rhs(*args, **kwargs)) 15 | 16 | def __rmul__(self, lhs): 17 | return lambda *args, **kwargs: lhs(self(*args, **kwargs)) 18 | 19 | @infix 20 | def compose(*args): 21 | return reduce(lambda acc, fn: 22 | (lambda *ag, **kwag: acc(fn(*ag, **kwag))), 23 | args) 24 | 25 | o = compose 26 | -------------------------------------------------------------------------------- /syntax_sugar/_match.py: -------------------------------------------------------------------------------- 1 | from ._pipe import END 2 | 3 | __all__ = [ 4 | 'match', 5 | ] 6 | 7 | class match: 8 | def __init__(self, x): 9 | self.x = x 10 | self.done = False 11 | self.result = None 12 | 13 | def __or__(self, rhs): 14 | if rhs is END: 15 | return self.result 16 | if self.done: 17 | return self 18 | if isinstance(rhs, dict): 19 | result = rhs.get(self.x, None) 20 | if result: 21 | self.done = True 22 | self.result = result 23 | return self 24 | if isinstance(rhs, tuple): 25 | patt, fn = rhs 26 | if self.x == patt: 27 | self.done = True 28 | self.result = fn() 29 | return self 30 | else: 31 | raise SyntaxError('Bad match syntax.') 32 | -------------------------------------------------------------------------------- /tests/test_iter.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from syntax_sugar._iter import Range, Iterator, INF 3 | from syntax_sugar._util import take, to 4 | from itertools import product 5 | 6 | 7 | def test_range_bad_step(): 8 | range_obj = Range(1, 2) 9 | for bad_step in ["x", [], 1.5]: 10 | with raises(TypeError): 11 | range_obj.step = bad_step 12 | 13 | with raises(ValueError): 14 | range_obj.step = 0 15 | 16 | with raises(ValueError): 17 | range_obj.step = -1 18 | 19 | range_obj = Range(2, 1) 20 | with raises(ValueError): 21 | range_obj.step = 1 22 | 23 | def test_iterator(): 24 | assert list(Iterator(Range(1, 10))) == list(range(1, 11)) 25 | assert list(Iterator(range(1, 10))) == list(range(1, 10)) 26 | assert list(Iterator([1,2,3,4])) == [1,2,3,4] 27 | assert list((1 /to/ INF) * (2 /to/ 4) /take/ 5) == list(product([1,2,3], [2,3,4]))[:5] 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | install: pip install . 5 | script: python setup.py pytest 6 | deploy: 7 | provider: pypi 8 | user: czheo 9 | password: 10 | secure: nf6zMORRla402yZpynyqnM7FHSFFg7DaTVaXV5857ZefPn0DM2r+5ZVoMqC/nAHeWygI+jRzmwORpOyviMcWSQRmWDTldKf9iddSw74e9jkgB+uDNoiVylVWhKSOzmQfZtEBfWFeBpnx8Vr60xp1l/fbYxmfHETkxwKFfQLAKPKnbEQMiFm7DUxPlXYuVmvEt7EL0xHd197KHAODFMHbcW/lLfgbjylgsVcpwC74sfmjaLRrzD5Y2NupCmjaJFtkgoWUhvU58vRRbu2M+lI4YpMCs8Ioi00o0KvOaoPnEho9pLsFNg8mbL0alZFLcWPOB0srb+XKhRWUSzv6Dd8SmYfO4U/Z6qrxN3lg0L59xuEeDAPN6WI60epVEf8YrMeJX6+yL2I82WeWHS3jim9RBLyxx8x2CbgCayP4U8Fhxt4vEtDUEOmh9fvtg2YRRbVpYjQQMutww+h9IinLh7NAkgujnylCu689JmsM7Mlhgh2bDYTTdx6i5LhPc2f4/+DmNmeNTmetL3WBe1xHFxnv5uX1d3FqH8nDKknEuDtfPXxZvAp+nr1FIS52b/4esNDMyZaD+x387X+TKRjQJ/3nWVBgQXGzSHsQ14J8ff2o63031lA1M/vlFUbS/c+ENaI2mCTAlzrXt/5XgvWYc3ewkoAEwTTc6hJvPhbxVWGy1Fk= 11 | on: 12 | tags: true 13 | distributions: sdist bdist_wheel 14 | repo: czheo/syntax_sugar_python 15 | python: 3.6 16 | -------------------------------------------------------------------------------- /syntax_sugar/_util.py: -------------------------------------------------------------------------------- 1 | from ._infix import infix 2 | from ._iter import Iterator, Range 3 | 4 | __all__ = [ 5 | 'flip', 6 | 'is_a', 7 | 'as_a', 8 | 'to', 9 | 'step', 10 | 'has', 11 | 'take', 12 | 'drop', 13 | ] 14 | 15 | def flip(fn): 16 | def wrapper(*args): 17 | return fn(args[1], args[0]) 18 | return wrapper 19 | 20 | is_a = infix(isinstance) 21 | has = infix(hasattr) 22 | 23 | @infix 24 | def as_a(obj, clazz): 25 | return clazz(obj) 26 | 27 | @infix 28 | def to(start, end): 29 | return Iterator(Range(start, end)) 30 | 31 | @infix 32 | def step(obj, step): 33 | step = abs(step) 34 | obj = Iterator(obj) if not obj /is_a/ Iterator else obj 35 | return obj.slice(step=step) 36 | 37 | @infix 38 | def take(obj, n): 39 | obj = Iterator(obj) if not obj /is_a/ Iterator else obj 40 | return obj.slice(stop=n) 41 | 42 | @infix 43 | def drop(obj, n): 44 | obj = Iterator(obj) if not obj /is_a/ Iterator else obj 45 | return obj.slice(start=n) 46 | -------------------------------------------------------------------------------- /syntax_sugar/_stream.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from inspect import getfullargspec 3 | 4 | __all__ = [ 5 | 'stream', 6 | ] 7 | 8 | class StreamIter: 9 | def __init__(self, fn, data): 10 | self.fn = fn 11 | self.fn_argc = len(getfullargspec(fn).args) 12 | self.data = data 13 | self.prev = [] 14 | 15 | def __iter__(self): 16 | return self 17 | 18 | def __next__(self): 19 | try: 20 | ret = next(self.data) 21 | self.prev.append(ret) 22 | while len(self.prev) > self.fn_argc: 23 | self.prev.pop(0) 24 | return ret 25 | except StopIteration: 26 | ret = self.fn(*self.prev) 27 | self.prev.append(ret) 28 | while len(self.prev) > self.fn_argc: 29 | self.prev.pop(0) 30 | return ret 31 | 32 | class stream: 33 | def __init__(self, data = None): 34 | if data is None: 35 | self.data = iter([]) 36 | else: 37 | self.data = iter(data) 38 | 39 | def __iter__(self): 40 | return self 41 | 42 | def __next__(self): 43 | return next(self.data) 44 | 45 | def __lshift__(self, rhs): 46 | if hasattr(rhs, '__iter__'): 47 | self.data = chain(self.data, rhs) 48 | elif hasattr(rhs, '__call__'): 49 | self.data = StreamIter(rhs, self.data) 50 | return self 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | .venv/ 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /tests/test_pipe.py: -------------------------------------------------------------------------------- 1 | from syntax_sugar._pipe import * 2 | from functools import partial, reduce 3 | import os 4 | 5 | def test_pipe_with_number(): 6 | assert pipe(10) | END == 10 7 | 8 | def test_pipe_with_list(): 9 | assert pipe([1,2,3,4]) | END == [1,2,3,4] 10 | 11 | def test_each(): 12 | assert pipe(range(10)) | each(lambda x: x**2) | END \ 13 | == [x**2 for x in range(10)] 14 | 15 | def test_puts(): 16 | assert pipe(10) | range | puts | END == range(10) 17 | 18 | def test_short_partial(): 19 | assert pipe(range(10)) | (map, lambda x: x**2) | list | END \ 20 | == [x**2 for x in range(10)] 21 | 22 | def test_pipe_connect(): 23 | p1 = pipe(10) | range | each(lambda x: x**2) 24 | p2 = pipe() | partial(reduce, lambda acc, x: acc + x) 25 | assert p1 | p2 | END == reduce(lambda acc,x : acc + x, map(lambda x: x**2, range(10))) 26 | 27 | p3 = pipe() | range | sum 28 | assert p1 | p2 | p3 | END == sum(range(reduce(lambda acc,x : acc + x, map(lambda x: x**2, range(10))))) 29 | 30 | # not all processes in the pool are necessarily used 31 | # 32 | def test_pipe_multiprocess(): 33 | assert pipe(10) | p[lambda x: x**2] | END == 10**2 34 | assert pipe(10) | p[lambda x: x**2] * 2 | END == 10**2 35 | assert pipe(100) | range | p[lambda x: x**2] * 3 | sorted | END == [x ** 2 for x in range(100)] 36 | 37 | def test_pipe_multithread(): 38 | assert pipe(10) | t[lambda x: x**2] | END == 10**2 39 | assert pipe(10) | t[lambda x: x**2] * 2 | END == 10**2 40 | assert pipe(100) | range | t[lambda x: x**2] * 3 | sorted | END == [x ** 2 for x in range(100)] 41 | 42 | def test_pipe_multigreenthread(): 43 | assert pipe(10) | [lambda x: x**2] | END == 10**2 44 | assert pipe(10) | [lambda x: x**2] * 2 | END == 10**2 45 | assert pipe(10) | g[lambda x: x**2] | END == 10**2 46 | assert pipe(10) | g[lambda x: x**2] * 2 | END == 10**2 47 | assert pipe(10000) | range | g[lambda x: x**2] * 10000 | sorted | END == [x ** 2 for x in range(10000)] 48 | 49 | def test_redirect(): 50 | p = pipe(range(10)) | each(str) | ''.join 51 | result = p | END 52 | # test file 53 | filename = 'testfile.txt' 54 | p > filename 55 | assert open(filename).read() == result 56 | p >> filename 57 | assert open(filename).read() == result * 2 58 | # clean test file 59 | os.remove(filename) 60 | -------------------------------------------------------------------------------- /syntax_sugar/_iter.py: -------------------------------------------------------------------------------- 1 | from ._pipe import pipe 2 | from itertools import tee, islice 3 | 4 | __all__ = [ 5 | 'INF', 6 | ] 7 | 8 | INF = float('inf') 9 | 10 | class Iterator: 11 | def __init__(self, data): 12 | if hasattr(data, '__iter__'): 13 | self.data = iter(data) 14 | else: 15 | raise TypeError('input must be iterable data') 16 | 17 | def __or__(self, rhs): 18 | return pipe(self) | rhs 19 | 20 | def __iter__(self): 21 | return self 22 | 23 | def __mul__(self, rhs): 24 | def product(rhs): 25 | for e1 in self: 26 | rhs, rhs_copy = tee(rhs) 27 | for e2 in rhs_copy: 28 | yield (e1, e2) 29 | return Iterator(product(rhs)) 30 | 31 | def __next__(self): 32 | return next(self.data) 33 | 34 | def slice(self, start=0, stop=None, step=1): 35 | return Iterator(islice(self.data, start, stop, step)) 36 | 37 | class Range: 38 | def __init__(self, start, end): 39 | if start in {INF, -INF}: 40 | raise ValueError('Cannot start range from infinity') 41 | 42 | if end == Ellipsis: 43 | end = INF 44 | 45 | valid_char = lambda c: isinstance(c, str) and len(c) == 1 46 | valid_integer = lambda i: isinstance(i, int) or i == INF or i == -INF 47 | 48 | if valid_integer(start) and valid_integer(end): 49 | self.type = 'number' 50 | elif valid_char(start) and valid_char(end): 51 | self.type = 'char' 52 | else: 53 | raise TypeError('Unknown range: %s to %s' % (start, end)) 54 | 55 | self.start = start 56 | self.curr = self.start 57 | self._step = 1 if end > start else -1 58 | self.end = end 59 | 60 | @property 61 | def step(self): 62 | return self._step 63 | 64 | @step.setter 65 | def step(self, value): 66 | if not isinstance(value, int): 67 | raise TypeError('Step must be int') 68 | elif value == 0: 69 | raise ValueError('Step cannot be zero') 70 | elif self.start < self.end and value < 0: 71 | raise ValueError('Increasing range with negative step') 72 | elif self.start > self.end and value > 0: 73 | raise ValueError('Decreasing range with positive step') 74 | 75 | self._step = value 76 | 77 | def __iter__(self): 78 | return self 79 | 80 | def __next__(self): 81 | def next_number(): 82 | too_big = self.step > 0 and self.curr > self.end 83 | too_small = self.step < 0 and self.curr < self.end 84 | 85 | if too_big or too_small: raise StopIteration 86 | 87 | ret = self.curr 88 | self.curr += self.step 89 | return ret 90 | 91 | def next_char(): 92 | too_big = self.step > 0 and ord(self.curr) > ord(self.end) 93 | too_small = self.step < 0 and ord(self.curr) < ord(self.end) 94 | 95 | if too_big or too_small: raise StopIteration 96 | 97 | ret = self.curr 98 | self.curr = chr(ord(self.curr) + self.step) 99 | return ret 100 | 101 | if self.type == 'number': 102 | return next_number() 103 | elif self.type == 'char': 104 | return next_char() 105 | else: 106 | raise StopIteration 107 | 108 | def __str__(self): 109 | if self.type == 'number': 110 | return super(Range, self).__str__() 111 | elif self.type == 'char': 112 | return ''.join(self) 113 | else: 114 | raise NotImplementedError 115 | -------------------------------------------------------------------------------- /tests/test_infix.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | import random 3 | from syntax_sugar import * 4 | from syntax_sugar._iter import Iterator 5 | 6 | def _assert_iter(expr, expected): 7 | assert expr /is_a/ Iterator == True 8 | assert list(expr) == list(expected) 9 | 10 | def test_int_to_int(): 11 | _assert_iter(1 /to/ 1, [1]) 12 | _assert_iter(2 /to/ 1, [2, 1]) 13 | for i in range(100): 14 | start, end = random.randint(1, 1e3), random.randint(1, 1e3) 15 | end += start 16 | _assert_iter(start /to/ end, range(start, end + 1)) 17 | 18 | start, end = end, start 19 | _assert_iter(start /to/ end, range(start, end - 1, -1)) 20 | 21 | def test_int_to_int_with_step(): 22 | _assert_iter(1 /to/ 1 /step/ 2, [1]) 23 | _assert_iter(2 /to/ 1 /step/ 2, [2]) 24 | for i in range(100): 25 | start, end = random.randint(1, 1e3), random.randint(1, 1e3) 26 | s = random.randint(1, 10) 27 | end += start 28 | _assert_iter(start /to/ end /step/ s, range(start, end + 1, s)) 29 | _assert_iter(start /to/ end /step/ -s, range(start, end + 1, s)) 30 | 31 | start, end = end, start 32 | _assert_iter(start /to/ end /step/ -s, range(start, end - 1, -s)) 33 | _assert_iter(start /to/ end /step/ s, range(start, end - 1, -s)) 34 | 35 | def test_str_to_str(): 36 | _assert_iter('A' /to/ 'Z', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') 37 | _assert_iter('Z' /to/ 'A', 'ZYXWVUTSRQPONMLKJIHGFEDCBA') 38 | _assert_iter('a' /to/ 'z', 'abcdefghijklmnopqrstuvwxyz') 39 | _assert_iter('z' /to/ 'a', 'zyxwvutsrqponmlkjihgfedcba') 40 | _assert_iter('D' /to/ 'V', 'DEFGHIJKLMNOPQRSTUV') 41 | _assert_iter('V' /to/ 'D', 'VUTSRQPONMLKJIHGFED') 42 | _assert_iter('v' /to/ 'd', 'vutsrqponmlkjihgfed') 43 | 44 | def test_str_to_str_with_step(): 45 | _assert_iter('A' /to/ 'Z' /step/ 3, 'ADGJMPSVY') 46 | _assert_iter('A' /to/ 'Z' /step/ -3, 'ADGJMPSVY') 47 | _assert_iter('Z' /to/ 'A' /step/ -3, 'ZWTQNKHEB') 48 | _assert_iter('Z' /to/ 'A' /step/ 3, 'ZWTQNKHEB') 49 | _assert_iter('a' /to/ 'z' /step/ 4, 'aeimquy') 50 | _assert_iter('a' /to/ 'z' /step/ -4, 'aeimquy') 51 | _assert_iter('z' /to/ 'a' /step/ -4, 'zvrnjfb') 52 | _assert_iter('z' /to/ 'a' /step/ 4, 'zvrnjfb') 53 | _assert_iter('D' /to/ 'V' /step/ 5, 'DINS') 54 | _assert_iter('D' /to/ 'V' /step/ -5, 'DINS') 55 | _assert_iter('V' /to/ 'D' /step/ -5, 'VQLG') 56 | _assert_iter('V' /to/ 'D' /step/ 5, 'VQLG') 57 | _assert_iter('v' /to/ 'd' /step/ -3, 'vspmjgd') 58 | _assert_iter('v' /to/ 'd' /step/ 3, 'vspmjgd') 59 | 60 | def test_infinity(): 61 | with raises(ValueError): 62 | INF /to/ 100 63 | 64 | with raises(ValueError): 65 | -INF /to/ 1 66 | 67 | _assert_iter(1 /to/ INF /take/ 10, range(1, 11)) 68 | _assert_iter(1 /to/ -INF /take/ 10, range(1, -9, -1)) 69 | 70 | _assert_iter(1 /to/ INF /step/ 2 /take/ 10, list(range(1, 100, 2))[:10]) 71 | _assert_iter(1 /to/ -INF /step/ 2 /take/ 10, list(range(1, -100, -2))[:10]) 72 | 73 | _assert_iter(1 /to/ -INF /step/ 2 /take/ 10 /drop/ 2, list(range(1, -100, -2))[2:10]) 74 | _assert_iter(1 /to/ -INF /step/ 2 /drop/ 5 /take/ 3, [-9, -11, -13]) 75 | 76 | 77 | def test_take(): 78 | _assert_iter(1 /to/ INF /take/ 5, [1,2,3,4,5]) 79 | _assert_iter(range(10) /take/ 5, [0,1,2,3,4]) 80 | _assert_iter([1,2,3,4,5,6,7] /take/ 5, [1,2,3,4,5]) 81 | 82 | def test_drop(): 83 | _assert_iter(1 /to/ 10 /drop/ 2 /take/ 3, [3, 4, 5]) 84 | _assert_iter(1 /to/ INF /drop/ 2 /take/ 3, [3, 4, 5]) 85 | _assert_iter(10 /to/ 1 /drop/ 3 /take/ 2, [7, 6]) 86 | 87 | def test_iter_pipe(): 88 | assert 0 /to/ 9 | each(lambda x: x**2) | END == [x**2 for x in range(10)] 89 | assert 0 /to/ 9 /take/ 2 | each(lambda x: x**2) | END == [x**2 for x in range(10)][:2] 90 | assert 0 /to/ 9 /drop/ 2 | each(lambda x: x**2) | END == [x**2 for x in range(10)][2:] 91 | 92 | def test_is_a(): 93 | values_types_right = [ 94 | (2, int), 95 | ('strings', str), 96 | ({}, dict), 97 | ([], list), 98 | ((), tuple) 99 | ] 100 | 101 | values_types_wrong = [ 102 | (2, [str, dict, list, tuple]), 103 | ('strings', [int, dict, list, tuple]), 104 | ({}, [int, str, list, tuple]), 105 | ([], [int, str, dict, tuple]), 106 | ((), [int, str, dict, list]) 107 | ] 108 | 109 | for value, type in values_types_right: 110 | assert value /is_a/ type 111 | 112 | for value, types in values_types_wrong: 113 | for type in types: 114 | assert not value /is_a/ type 115 | 116 | def test_as_a(): 117 | assert 1 /as_a/ str == '1' 118 | -------------------------------------------------------------------------------- /syntax_sugar/_pipe.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from ._composable import compose, composable 3 | from multiprocess.pool import ThreadPool, Pool 4 | from eventlet import GreenPool 5 | 6 | __all__ = [ 7 | 'END', 8 | 'DEBUG', 9 | 'pipe', 10 | 'each', 11 | 'puts', 12 | 'process_syntax', 13 | 'thread_syntax', 14 | 'green_thread_syntax', 15 | 'p', 16 | 't', 17 | 'g', 18 | ] 19 | 20 | def puts(data, end="\n"): 21 | print(data, end=end) 22 | return data 23 | 24 | def each(fn): 25 | return compose(list, partial(map, fn)) 26 | 27 | class End: 28 | "mark end of pipe" 29 | pass 30 | 31 | END = End() 32 | 33 | class Debug: 34 | "mark end of pipe for debug" 35 | pass 36 | 37 | DEBUG = Debug() 38 | 39 | class MultiTaskSyntax: 40 | def __init__(self): 41 | self.poolsize = 1 42 | def __mul__(self, other): 43 | self.poolsize = other 44 | return self 45 | def __getitem__(self, func): 46 | self.func = func 47 | return self 48 | 49 | class ProcessSyntax(MultiTaskSyntax): 50 | pass 51 | 52 | class ThreadSyntax(MultiTaskSyntax): 53 | pass 54 | 55 | class GreenThreadSyntax(MultiTaskSyntax): 56 | pass 57 | 58 | class EventSyntax(MultiTaskSyntax): 59 | pass 60 | 61 | def multitask(fn, poolsize, data, pool_constructor): 62 | with pool_constructor(poolsize) as p: 63 | if not hasattr(data, '__iter__'): 64 | data= [data] 65 | return p.map(fn, data)[0] 66 | else: 67 | return p.map(fn, data) 68 | 69 | def multiprocess(fn, poolsize, data): 70 | return multitask(fn, poolsize, data, Pool) 71 | 72 | def multithread(fn, poolsize, data): 73 | return multitask(fn, poolsize, data, ThreadPool) 74 | 75 | def multigreenthread(fn, poolsize, data): 76 | p = GreenPool(poolsize) 77 | if not hasattr(data, '__iter__'): 78 | data= [data] 79 | return next(p.imap(fn, data)) 80 | else: 81 | return list(p.imap(fn, data)) 82 | 83 | 84 | process_syntax = p = ProcessSyntax() 85 | thread_syntax = t = ThreadSyntax() 86 | green_thread_syntax = g = GreenThreadSyntax() 87 | 88 | class pipe: 89 | def __init__(self, data = None): 90 | # default action does nothing 91 | self.action = lambda x: x 92 | self.data = data 93 | 94 | def __call__(*args): 95 | self = args[0] 96 | args = args[1:] 97 | if len(args) == 1: 98 | return self.action(args[0]) 99 | elif len(args) == 0: 100 | return self.action(self.data) 101 | else: 102 | raise TypeError('pipe takes 0 or 1 argument but %d are given' % len(args)) 103 | 104 | def start(self, rhs): 105 | # pipe start 106 | self.data = rhs 107 | self.pipein = True 108 | 109 | def function(self, rhs): 110 | self.data = rhs(self.data) 111 | 112 | def __or__(self, rhs): 113 | if rhs is END: 114 | # end of pipe 115 | return self.action(self.data) 116 | elif rhs is DEBUG: 117 | # debug end of pipe 118 | try: 119 | return self.action(self.data) 120 | except Exception as e: 121 | return e 122 | elif isinstance(rhs, list): 123 | if len(set(rhs)) != 1: 124 | raise SyntaxError('Bad pipe multiprocessing syntax.') 125 | poolsize = len(rhs) 126 | new_action = rhs[0] 127 | self.action = compose(partial(multithread, new_action, poolsize), self.action) 128 | elif isinstance(rhs, ProcessSyntax): 129 | self.action = compose(partial(multiprocess, rhs.func, rhs.poolsize), self.action) 130 | elif isinstance(rhs, ThreadSyntax): 131 | self.action = compose(partial(multithread, rhs.func, rhs.poolsize), self.action) 132 | elif isinstance(rhs, GreenThreadSyntax): 133 | self.action = compose(partial(multigreenthread, rhs.func, rhs.poolsize), self.action) 134 | elif isinstance(rhs, tuple): 135 | self.action = compose(partial(*rhs), self.action) 136 | elif isinstance(rhs, pipe): 137 | # connect another pipe 138 | new_pipe = pipe(self.data) | compose(rhs.action, self.action) 139 | return new_pipe 140 | elif hasattr(rhs, '__call__'): 141 | # middle of pipe 142 | self.action = compose(rhs, self.action) 143 | else: 144 | raise SyntaxError('Bad pipe %s' % rhs) 145 | return self 146 | 147 | def __gt__(self, rhs): 148 | "pipe > 'filename'" 149 | with open(rhs, 'w') as f: 150 | f.write(str(self.action(self.data))) 151 | 152 | def __rshift__(self, rhs): 153 | "pipe >> 'filename'" 154 | with open(rhs, 'a') as f: 155 | f.write(str(self.action(self.data))) 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # syntax_sugar [![travis_status](https://travis-ci.org/czheo/syntax_sugar_python.svg?branch=master)](https://travis-ci.org/czheo/syntax_sugar_python) [![PyPI](https://img.shields.io/pypi/v/syntax_sugar.svg)](https://pypi.python.org/pypi/syntax_sugar) 2 | 3 | This lib adds some anti-Pythonic "syntactic sugar" to Python. 4 | 5 | NOTE: This is merely an experimental prototype to show some potential of operator overloading in Python. Only tested under Python 3.6.0. Anything may evolve without announcement in advance. 6 | 7 | Inspired by https://github.com/matz/streem. 8 | 9 | Also, you can watch the last part of this Matz's talk to understand the intuition behind this project. 10 | 11 | [![Stream Model](https://img.youtube.com/vi/48iKjUcENRE/0.jpg)](https://youtu.be/48iKjUcENRE?t=39m29s) 12 | 13 | # Install 14 | ``` 15 | pip install syntax_sugar 16 | ``` 17 | 18 | # Use 19 | 20 | To test out this lib, you can simply do. 21 | 22 | ``` python 23 | from syntax_sugar import * 24 | ``` 25 | 26 | For serious use, you can explicitly import each component as explained below ... if you dare to use this lib. 27 | 28 | ### pipe 29 | ``` python 30 | from syntax_sugar import pipe, END 31 | from functools import partial 32 | 33 | pipe(10) | range | partial(map, lambda x: x**2) | list | print | END 34 | # put 10 into the pipe and just let data flow. 35 | # output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 36 | # remember to call END at the end 37 | # NOTE: everything in the middle of the pipe is just normal Python functions 38 | 39 | pipe(10) | range | (map, lambda x: x**2) | list | print | END 40 | # Tuples are shortcuts for partial functions 41 | 42 | from syntax_sugar import each 43 | x = pipe(10) | range | each(lambda x: x ** 2) | END 44 | # We can also save the result in a variable. 45 | # `each` is an eager evaluated version of the partial function of `map`, which returns a list instead of a map object. (Equivalent to `map` in Python 2) 46 | 47 | pipe(10) | range | each(str) | ''.join > 'test.txt' 48 | # wanna write to a file? Why not! 49 | # write "0123456789" to test.txt 50 | # We don't need to put END here. 51 | ``` 52 | 53 | We can connect multiple pipes to create a longer pipe 54 | 55 | ``` python 56 | from syntax_sugar import pipe, each, END 57 | from functools import reduce 58 | 59 | p1 = pipe(10) | range | each(lambda x: x/2) 60 | # head pipe can have input value 61 | p2 = pipe() | (reduce, lambda acc, x: (acc + x)/2) 62 | p3 = pipe() | int | range | sum 63 | # middle pipes can have no input value 64 | 65 | p1 | p2 | p3 | END 66 | # returns 6 67 | 68 | 69 | p = p1 | p2 | p3 70 | p() 71 | # You can invoke the pipe by calling it as a function 72 | 73 | # you can also put a different value in the pipe 74 | p(20) 75 | # returns 36 76 | ``` 77 | 78 | ### pipe with parallelism 79 | 80 | By default, pipe works with threads. 81 | 82 | You can have a function running in a seperate thread with pipe. Just put it in a `[]` or more explicitly `t[]`. Threads and processes are also available. 83 | 84 | ``` python 85 | from syntax_sugar import (thread_syntax as t, 86 | process_syntax as p) 87 | 88 | pipe(10) | [print] | END # print run in a thread 89 | pipe(10) | t[print] | END # print run in a thread 90 | pipe(10) | p[print] | END # print run in a process 91 | ``` 92 | 93 | What makes this syntax good is that you can specify how many threads you want to spawn, by doing `[function] * n` where `n` is the number of threads. 94 | 95 | ``` python 96 | pipe([1,2,3,4,5]) | [print] * 3 | END # print will run in a ThreadPool of size 3 97 | ``` 98 | 99 | Here is an example of requesting a list of urls in parallel 100 | 101 | ``` python 102 | import requests 103 | (pipe(['google', 'twitter', 'yahoo', 'facebook', 'github']) 104 | | each(lambda name: 'http://' + name + '.com') 105 | | [requests.get] * 3 # !! `requests.get` runs in a ThreadPool of size 3 106 | | each(lambda resp: (resp.url, resp.headers.get('Server'))) 107 | | list 108 | | END) 109 | 110 | # returns 111 | # [('http://www.google.com/', 'gws'), 112 | # ('https://twitter.com/', 'tsa_a'), 113 | # ('https://www.yahoo.com/', 'ATS'), 114 | # ('https://www.facebook.com/', None), 115 | # ('https://github.com/', 'GitHub.com')] 116 | ``` 117 | 118 | ### infix function 119 | ``` python 120 | from syntax_sugar import is_a, has, to, step, drop 121 | 122 | 1 /is_a/ int 123 | # equivalent to `isinstance(1, int)` 124 | 125 | 1 /as_a/ str 126 | # "1" 127 | 128 | range(10) /has/ '__iter__' 129 | # equivalent to `hasattr(range(10), "__iter__")` 130 | 131 | 1 /to/ 10 132 | # An iterator similar to `range(1, 11)`. 133 | # Python's nasty range() is right-exclusive. This is right-inclusive. 134 | 135 | 10 /to/ 1 136 | # We can go backward. 137 | 138 | '0' /to/ '9' 139 | # We can also have a range of characters :) 140 | 141 | 1 /to/ 10 /step/ 2 142 | # We can also specify step sizes. 143 | # Similar to `range(1, 11, 2)` 144 | 145 | 10 /to/ 1 /step/ 2 146 | # Go backward. 147 | # Similar to `range(10, 0, -2)` 148 | 149 | 1 /to/ 10 /drop/ 5 150 | # there is a `drop` functon which drop N items from the head 151 | # An iterator similar to [6, 7, 8, 9, 10] 152 | ``` 153 | 154 | `/to/` has some advanced features 155 | 156 | - lazy evaluation. 157 | - support infinity. 158 | - support product operation. 159 | - support pipe. 160 | 161 | ``` python 162 | from syntax_sugar import INF, take, each 163 | 164 | # CAUTION: this will infinitely print numbers 165 | for i in 1 /to/ INF: 166 | print(i) 167 | 168 | 1 /to/ INF /take/ 5 /as_a/ list 169 | # there is a `take` functon which is similar to itertools.islice 170 | # return [1, 2, 3, 4, 5] 171 | 172 | 1 /to/ ... /take/ 5 /as_a/ list 173 | # ... is equivalent to INF 174 | 175 | 0 /to/ -INF /step/ 2 /take/ 5 /as_a/ list 176 | # also works with negative infinity. 177 | # return [0, -2, -4, -6, -8] 178 | 179 | (1 /to/ 3) * (4 /to/ 6) /as_a/ list 180 | # all combinations of [1..3] * [4..6] 181 | # return [(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)] 182 | 183 | 1 /to/ 10 /take/ 5 | each(lambda x: x **2) | END 184 | # These infix functions can also be piped. 185 | # [1, 4, 9, 16, 25] 186 | ``` 187 | 188 | Make your own infix function, so you can append multiple items to a list in one line. 189 | 190 | ``` python 191 | from syntax_sugar import infix 192 | 193 | @infix 194 | def push(lst, x): 195 | lst.append(x) 196 | return lst 197 | 198 | [] /push/ 1 /push/ 2 /push/ 3 199 | # returns [1,2,3] 200 | ``` 201 | 202 | You can also do 203 | 204 | ``` python 205 | def push(lst, x): 206 | lst.append(x) 207 | return lst 208 | 209 | ipush = push /as_a/ infix 210 | 211 | [] /ipush/ 1 /ipush/ 2 /ipush/ 3 212 | # returns [1,2,3] 213 | ``` 214 | 215 | 231 | 232 | 233 | ### function composition 234 | 235 | In math, `(f * g) (x) = f(g(x))`. This is called function composition. 236 | 237 | ``` python 238 | # lmap equivalent to `list(map(...))` 239 | lmap = compose(list, map) 240 | lmap(lambda x: x ** 2, range(10)) 241 | ``` 242 | 243 | Let's say we want to represent `f * g * h` in a program, i.e. `fn(x) = f(g(h(x)))` 244 | 245 | ``` python 246 | f = lambda x: x**2 + 1 247 | g = lambda x: 2*x - 1 248 | h = lambda x: -2 * x**3 + 3 249 | 250 | fn = compose(f, g, h) 251 | 252 | fn(5) # 245026 253 | ``` 254 | 255 | or you can do 256 | 257 | ```python 258 | f = composable(lambda x: x**2 + 1) 259 | g = composable(lambda x: 2*x - 1) 260 | h = composable(lambda x: -2 * x**3 + 3) 261 | 262 | fn = f * g * h 263 | 264 | fn(5) # 245026 265 | ``` 266 | 267 | Sometimes you may prefer the decorator way. 268 | 269 | ``` python 270 | # make your own composable functions 271 | @composable 272 | def add2(x): 273 | return x + 2 274 | 275 | @composable 276 | def mul3(x): 277 | return x * 3 278 | 279 | @composable 280 | def pow2(x): 281 | return x ** 2 282 | 283 | fn = add2 * mul3 * pow2 284 | # equivalent to `add2(mul3(pow2(n)))` 285 | fn(5) 286 | # returns 5^2 * 3 + 2 = 77 287 | ``` 288 | 289 | More receipes: https://github.com/czheo/syntax_sugar_python/tree/master/recipes 290 | --------------------------------------------------------------------------------