def test_pep(): if sys.version_info < (3, 8): pytest.skip('No Assignment expressions in python < 3.8') source = ''' def name(p1, p2, /, p_or_kw, *, kw): pass def name(p1, p2=None, /, p_or_kw=None, *, kw): pass def name(p1, p2=None, /, *, kw): pass def name(p1, p2=None, /): pass def name(p1, p2, /, p_or_kw): pass def name(p1, p2, /): pass def name(p_or_kw, *, kw): pass def name(*, kw): pass def standard_arg(arg): print(arg) def pos_only_arg(arg, /): print(arg) def kwd_only_arg(*, arg): print(arg) def combined_example(pos_only, /, standard, *, kwd_only): print(pos_only, standard, kwd_only) ''' expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def test_match_unparse(): if sys.version_info < (3, 10): pytest.skip('Match statement not in python < 3.10') source = ''' match a: case (0 as a) as b: pass match a: case _:pass match a: case 0|(0|0): pass case (0|0)|0: pass case 0|0|0: pass match (lambda: a)(): case [action, obj]:pass match a:= h: case [action, obj]:pass case {**rest}: pass ''' expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def assert_code(expected_ast, actual_ast): try: compare_ast(expected_ast, actual_ast) except CompareError as e: print(e) print(unparse(actual_ast)) raise
def test_file(path): try: with open(path, 'rb') as f: source = f.read() except IOError: pytest.skip('IOError opening file') try: original_ast = ast.parse(source, path) except SyntaxError: pytest.skip('Invalid syntax in file') # Test unparsing unparse(original_ast) # Test transforms minify(source, filename=path)
def test_pep(): if sys.version_info < (3, 9): pytest.skip('Decorator expression not allowed in python <3.9') source = """ buttons = [QPushButton(f'Button {i}') for i in range(10)] # Do stuff with the list of buttons... @buttons[0].clicked.connect def spam(): ... @buttons[1].clicked.connect def eggs(): ... # Do stuff with the list of buttons... @(f, g) def a(): pass @(f, g) class A:pass @lambda func: (lambda *p: func(*p).u()) def g(n): pass @s := lambda func: (lambda *p: func(*p).u()) def g(name): pass @s def r(n, t): pass @lambda f: lambda *p: f or f(*p).u() def g(name): pass @lambda f: lambda *p: \ [_ for _ in [ \ f(*p), ] if _][0] def c(): pass @lambda f: lambda *p: \ list(filter(lambda _: _,[ (a := t()) and False, f(*p), (b := t()) and False, ]))[0] def c(): pass """ expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def hoist(source): module = ast.parse(source) add_namespace(module) bind_names(module) resolve_names(module) allow_rename_locals(module, False) allow_rename_globals(module, False) rename_literals(module) rename(module) print(unparse(module)) return module
def test_fstring_empty_str(): if sys.version_info < (3, 6): pytest.skip('f-string expressions not allowed in python < 3.6') source = r''' f"""\ {fg_br}""" ''' print(source) expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def test_await_fstring(): if sys.version_info < (3, 7): pytest.skip( 'Await in f-string expressions not allowed in python < 3.7') source = ''' async def a(): return 'hello' async def b(): return f'{await b()}' ''' expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def test_dict_expanson(): if sys.version_info < (3, 5): pytest.skip('dict expansion not allowed in python < 3.5') source = [ r'{**a>>9}', r'{**(a or b)}', r'{**(a and b)}', r'{**a+b}', r'{**(lambda a:a)}', r'{**(a<b)}', r'{**(yield a())}', r'{**(a if a else a)}' ] for expression in source: expected_ast = ast.parse(expression) minified = unparse(expected_ast) compare_ast(expected_ast, ast.parse(minified)) assert expression == minified
def test_return(): if sys.version_info < (3, 0): pytest.skip('Iterable unpacking in return not allowed in python < 3.0') elif sys.version_info < (3, 8): # Parenthesis are required source = 'def a():return(True,*[False])' else: # Parenthesis not required source = 'def a():return True,*[False]' expected_ast = ast.parse(source) minified = unparse(expected_ast) compare_ast(expected_ast, ast.parse(minified)) assert source == minified
def test_pep(): if sys.version_info < (3, 8): pytest.skip('No Assignment expressions in python < 3.8') source = ''' if a := True: print(a) if self._is_special and (ans := self._check_nans(context=context)): return ans results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0] stuff = [[y := f(x), x/y] for x in range(5)] ''' expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def test_slice(): """AST for slices was changed in 3.9""" source = ''' x[name] x[1:2] x[1:2, 3] x[()] x[1:2, 2:2] x[a, ..., b:c] x[a, ..., b] x[(a, b)] x[a:b,] ''' expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def test_pep635_unparse(): if sys.version_info < (3, 10): pytest.skip('Match statement not in python < 3.10') source = ''' match x: case host, port: mode = "http" case host, port, mode: pass match node: case BinOp("+", a, BinOp("*", b, c)): pass match json_pet: case {"type": "cat", "name": name, "pattern": pattern}: return Cat(name, pattern) case {"type": "dog", "name": name, "breed": breed}: return Dog(name, breed) case _: raise ValueError("Not a suitable pet") def sort(seq): match seq: case [] | [_]: return seq case [x, y] if x <= y: return seq case [x, y]: return [y, x] case [x, y, z] if x <= y <= z: return seq case [x, y, z] if x >= y >= z: return [z, y, x] case [p, *rest]: a = sort([x for x in rest if x <= p]) b = sort([x for x in rest if p < x]) return a + [p] + b def simplify_expr(tokens): match tokens: case [('('|'[') as l, *expr, (')'|']') as r] if (l+r) in ('()', '[]'): return simplify_expr(expr) case [0, ('+'|'-') as op, right]: return UnaryOp(op, right) case [(int() | float() as left) | Num(left), '+', (int() | float() as right) | Num(right)]: return Num(left + right) case [(int() | float()) as value]: return Num(value) def simplify(expr): match expr: case ('/', 0, 0): return expr case ('*'|'/', 0, _): return 0 case ('+'|'-', x, 0) | ('+', 0, x) | ('*', 1, x) | ('*'|'/', x, 1): return x return expr def simplify(expr): match expr: case ('+', 0, x): return x case ('+' | '-', x, 0): return x case ('and', True, x): return x case ('and', False, x): return False case ('or', False, x): return x case ('or', True, x): return True case ('not', ('not', x)): return x return expr def average(*args): match args: case [x, y]: # captures the two elements of a sequence return (x + y) / 2 case [x]: # captures the only element of a sequence return x case []: return 0 case a: # captures the entire sequence return sum(a) / len(a) def is_closed(sequence): match sequence: case [_]: # any sequence with a single element return True case [start, *_, end]: # a sequence with at least two elements return start == end case _: # anything return False def handle_reply(reply): match reply: case (HttpStatus.OK, MimeType.TEXT, body): process_text(body) case (HttpStatus.OK, MimeType.APPL_ZIP, body): text = deflate(body) process_text(text) case (HttpStatus.MOVED_PERMANENTLY, new_URI): resend_request(new_URI) case (HttpStatus.NOT_FOUND): raise ResourceNotFound() def change_red_to_blue(json_obj): match json_obj: case { 'color': ('red' | '#FF0000') }: json_obj['color'] = 'blue' case { 'children': children }: for child in children: change_red_to_blue(child) ''' expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))
def test_pep646_unparse(): if sys.version_info < (3, 10): pytest.skip('Match statement not in python < 3.10') source = ''' match command.split(): case [action]: pass case [action, obj]: pass match command.split(): case ["quit"]: print("Goodbye!") quit_game() case ["look"]: current_room.describe() case ["get", obj]: character.get(obj, current_room) case ["go", direction]: current_room = current_room.neighbor(direction) match command.split(): case ["drop", *objects]: for obj in objects: character.drop(obj, current_room) match command.split(): case ["quit"]: pass case ["go", direction]: pass case ["drop", *objects]: pass case _: print(f"Sorry, I couldn't understand {command!r}") match command.split(): case ["north"] | ["go", "north"]: current_room = current_room.neighbor("north") case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: pass match command.split(): case ["go", ("north" | "south" | "east" | "west")]: current_room = current_room.neighbor(...) match command.split(): case ["go", ("north" | "south" | "east" | "west") as direction]: current_room = current_room.neighbor(direction) match command.split(): case ["go", direction] if direction in current_room.exits: current_room = current_room.neighbor(direction) case ["go", _]: print("Sorry, you can't go that way") match event.get(): case Click(position=(x, y)): handle_click_at(x, y) case KeyPress(key_name="Q") | Quit(): game.quit() case KeyPress(key_name="up arrow"): game.go_north() case KeyPress(): pass # Ignore other keystrokes case other_event: raise ValueError(f"Unrecognized event: {other_event}") match event.get(): case Click((x, y)): handle_click_at(x, y) match event.get(): case Click((x, y), button=Button.LEFT): # This is a left click handle_click_at(x, y) case Click(): pass # ignore other clicks match action: case {"text": message, "color": c}: ui.set_text_color(c) ui.display(message) case {"sleep": duration}: ui.wait(duration) case {"sound": url, "format": "ogg"}: ui.play(url) case {"sound": _, "format": _}: warning("Unsupported audio format") match action: case {"text": str(message), "color": str(c)}: ui.set_text_color(c) ui.display(message) case {"sleep": float(duration)}: ui.wait(duration) case {"sound": str(url), "format": "ogg"}: ui.play(url) case {"sound": _, "format": _}: warning("Unsupported audio format") match status: case 400: return "Bad request" case 404: return "Not found" case 418: return "I'm a teapot" case 401 | 403 | 404: return "Not allowed" case _: return "Something's wrong with the Internet" match point: case (0, 0): print("Origin") case (0, y): print(f"Y={y}") case (x, 0): print(f"X={x}") case (x, y): print(f"X={x}, Y={y}") case _: raise ValueError("Not a point") match point: case Point(x=0, y=0): print("Origin") case Point(x=0, y=y): print(f"Y={y}") case Point(x=x, y=0): print(f"X={x}") case Point(): print("Somewhere else") case _: print("Not a point") match points: case []: print("No points") case [Point(0, 0)]: print("The origin") case [Point(x, y)]: print(f"Single point {x}, {y}") case [Point(0, y1), Point(0, y2)]: print(f"Two on the Y axis at {y1}, {y2}") case _: print("Something else") match point: case Point(x, y) if x == y: print(f"Y=X at {x}") case Point(x, y): print(f"Not on the diagonal") match color: case Color.RED: print("I see red!") case Color.GREEN: print("Grass is green") case Color.BLUE: print("I'm feeling the blues :(") ''' expected_ast = ast.parse(source) actual_ast = unparse(expected_ast) compare_ast(expected_ast, ast.parse(actual_ast))