8000 Merge pull request #563 from realpython/structural-pattern-matching · realpython/materials@ab4f4dc · GitHub
[go: up one dir, main page]

Skip to content

Commit ab4f4dc

Browse files
authored
Merge pull request #563 from realpython/structural-pattern-matching
Materials for Structural Pattern Matching: Initial Commit
2 parents 46e9df9 + 3814d95 commit ab4f4dc

File tree

9 files changed

+476
-0
lines changed

9 files changed

+476
-0
lines changed

structural-pattern-matching/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Structural Pattern Matching in Python
2+
3+
This folder contains the code samples for the Real Python tutorial [Structural Pattern Matching in Python](https://realpython.com/structural-pattern-matching/).
4+
5+
## Installation
6+
7+
Create and activate a virtual environment:
8+
9+
```sh
10+
$ python -m venv venv/
11+
$ source venv/bin/activate
12+
```
13+
14+
Install the required third-party dependencies:
15+
16+
```sh
17+
(venv) $ python -m pip install -r requirements.txt
18+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
2+
from sys import stderr
3+
from urllib.parse import ParseResult, urlparse
4+
5+
6+
def fetch(url):
7+
print 8000 (f"Fetching URL: {url}", file=stderr)
8+
connection = make_connection(url)
9+
try:
10+
connection.request("GET", "/")
11+
match connection.getresponse():
12+
case HTTPResponse(status=code) if code >= 400:
13+
raise ValueError("Failed to fetch URL")
14+
case HTTPResponse(status=code) as resp if (
15+
code >= 300 and (redirect := resp.getheader("Location"))
16+
):
17+
return fetch(redirect)
18+
case HTTPResponse(status=code) as resp if code >= 200:
19+
return resp.read()
20+
case _:
21+
raise ValueError("Unexpected response")
22+
finally:
23+
connection.close()
24+
25+
26+
def make_connection(url):
27+
match urlparse(url):
28+
case ParseResult("http", netloc=host):
29+
return HTTPConnection(host)
30+
case ParseResult("https", netloc=host):
31+
return HTTPSConnection(host)
32+
case _:
33+
raise ValueError("Unsupported URL scheme")
34+
35+
36+
if __name__ == "__main__":
37+
fetch("http://realpython.com/")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import random
2+
3+
MIN, MAX = 1, 100
4+
MAX_TRIES = 5
5+
PROMPT_1 = f"\N{mage} Guess a number between {MIN} and {MAX}: "
6+
PROMPT_2 = "\N{mage} Try again: "
7+
BYE = "Bye \N{waving hand sign}"
8+
9+
10+
def main():
11+
print("Welcome to the game! Type 'q' or 'quit' to exit.")
12+
while True:
13+
play_game()
14+
if not want_again():
15+
bye()
16+
17+
18+
def play_game():
19+
drawn_number = random.randint(MIN, MAX)
20+
num_tries = MAX_TRIES
21+
prompt = PROMPT_1
22+
while num_tries > 0:
23+
match input(prompt):
24+
case command if command.lower() in ("q", "quit"):
25+
bye()
26+
case user_input:
27+
try:
28+
user_number = int(user_input)
29+
except ValueError:
30+
print("That's not a number!")
31+
else:
32+
match user_number:
33+
case number if number < drawn_number:
34+
num_tries -= 1
35+
prompt = PROMPT_2
36+
print(f"Too low! {num_tries} tries left.")
37+
case number if number > drawn_number:
38+
num_tries -= 1
39+
prompt = PROMPT_2
40+
print(f"Too high! {num_tries} tries left.")
41+
case _:
42+
print("You won \N{party popper}")
43+
return
44+
print("You lost \N{pensive face}")
45+
46+
47+
def want_again():
48+
while True:
49+
match input("Do you want to play again? [Y/N] ").lower():
50+
case "y":
51+
return True
52+
case "n":
53+
return False
54+
55+
56+
def bye():
57+
print(BYE)
58+
exit()
59+
60+
61+
if __name__ == "__main__":
62+
try:
63+
main()
64+
except (KeyboardInterrupt, EOFError):
65+
print()
66+
bye()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import sys
2+
3+
4+
def interpret(code, num_bytes=2**10):
5+
stack, brackets = [], {}
6+
for i, instruction in enumerate(code):
7+
match instruction:
8+
case "[":
9+
stack.append(i)
10+
case "]":
11+
brackets[i], brackets[j] = (j := stack.pop()), i
12+
memory = bytearray(num_bytes)
13+
pointer = ip = 0
14+
while ip < len(code):
15+
match code[ip]:
16+
case ">":
17+
pointer += 1
18+
case "<":
19+
pointer -= 1
20+
case "+":
21+
memory[pointer] += 1
22+
case "-":
23+
memory[pointer] -= 1
24+
case ".":
25+
print(chr(memory[pointer]), end="")
26+
case ",":
27+
memory[pointer] = ord(sys.stdin.buffer.read(1))
28+
case "[" if memory[pointer] == 0:
29+
ip = brackets[ip]
30+
case "]" if memory[pointer] != 0:
31+
ip = brackets[ip]
32+
ip += 1
33+
34+
35+
if __name__ == "__main__":
36+
interpret(
37+
"""
38+
+++++++++++[>++++++>+++++++++>++++++++>++++>+++>+<<<<<<-]>+++
39+
+++.>++.+++++++..+++.>>.>-.<<-.<.+++.------.--------.>>>+.>-.
40+
"""
41+
)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import json
2+
import urllib.request
3+
from dataclasses import dataclass
4+
5+
from rich.console import Console
6+
from rich.markdown import Markdown
7+
from rich.panel import Panel
8+
9+
10+
@dataclass
11+
class Comment:
12+
when: str
13+
body: str
14+
url: str
15+
user: str
16+
user_url: str
17+
issue_title: str
18+
19+
@property
20+
def footer(self):
21+
return (
22+
f"Comment by [{self.user}]({self.user_url})"
23+
f" on [{self.when}]({self.url})"
24+
)
25+
26+
def render(self):
27+
return Panel(
28+
Markdown(f"{self.body}\n\n---\n_{self.footer}_"),
29+
title=self.issue_title,
30+
padding=1,
31+
)
32+
33+
34+
def fetch_github_events(org, repo):
35+
url = f"https://api.github.com/repos/{org}/{repo}/events"
36+
with urllib.request.urlopen(url) as response:
37+
return json.loads(response.read())
38+
39+
40+
def filter_comments(events):
41+
for event in events:
42+
match event:
43+
case {
44+
"type": "IssueCommentEvent",
45+
"created_at": when,
46+
"actor": {
47+
"display_login": user,
48+
},
49+
"payload": {
50+
"action": "created",
51+
"issue": {
52+
"state": "open",
53+
"title": issue_title,
54+
},
55+
"comment": {
56+
"body": body,
57+
"html_url": url,
58+
"user": {
59+
"html_url": user_url,
60+
},
61+
},
62+
},
63+
}:
64+
yield Comment(when, body, url, user, user_url, issue_title)
65+
66+
67+
def main():
68+
console = Console()
69+
events = fetch_github_events("python", "cpython")
70+
for comment in filter_comments(events):
71+
console.clear()
72+
console.print(comment.render())
73+
console.input("\nPress [b]ENTER[/b] for the next comment...")
74+
75+
76+
if __name__ == "__main__":
77+
main()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import ast
2+
import inspect
3+
import textwrap
4+
5+
6+
def main():
7+
source_code = inspect.getsource(sample_function)
8+
source_tree = ast.parse(source_code)
9+
target_tree = optimize(source_tree)
10+
target_code = ast.unparse(target_tree)
11+
12+
print("Before:")
13+
print(ast.dump(source_tree))
14+
print(textwrap.indent(source_code, "| "))
15+
16+
print("After:")
17+
print(ast.dump(target_tree))
18+
print(textwrap.indent(target_code, "| "))
19+
20+
21+
def sample_function():
22+
return 40 + 2
23+
24+
25+
def optimize(node):
26+
match node:
27+
case ast.Module(body, type_ignores):
28+
return ast.Module(
29+
[optimize(child) for child in body], type_ignores
30+
)
31+
case ast.FunctionDef():
32+
return ast.FunctionDef(
33+
name=node.name,
34+
args=node.args,
35+
body=[optimize(child) for child in node.body],
36+
decorator_list=node.decorator_list,
37+
returns=node.returns,
38+
type_comment=node.type_comment,
39+
type_params=node.type_params,
40+
lineno=node.lineno,
41+
)
42+
case ast.Return(value):
43+
return ast.Return(value=optimize(value))
44+
case ast.BinOp(ast.Constant(left), op, ast.Constant(right)):
45+
match op:
46+
case ast.Add():
47+
return ast.Constant(left + right)
48+
case ast.Sub():
49+
return ast.Constant(left - right)
50+
case _:
51+
return node
52+
case _:
53+
return node
54+
55+
56+
if __name__ == "__main__":
57+
main()

structural-pattern-matching/repl.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import ast
2+
import sys
3+
import traceback
4+
5+
PROMPT = "\N{snake} "
6+
COMMANDS = ("help", "exit", "quit")
7+
8+
9+
def main():
10+
print('Type "help" for more information, "exit" or "quit" to finish.')
11+
while True:
12+
try:
13+
match input(PROMPT):
14+
case command if command.lower() in COMMANDS:
15+
match command.lower():
16+
case "help":
17+
print(f"Python {sys.version}")
18+
case "exit" | "quit":
19+
break
20+
case expression if valid(expression, "eval"):
21+
_ = eval(expression)
22+
if _ is not None:
23+
print(_)
24+
case statement if valid(statement, "exec"):
25+
exec(statement)
26+
case _:
27+
print("Please type a command or valid Python")
28+
except KeyboardInterrupt:
29+
print("\nKeyboardInterrupt")
30+
except EOFError:
31+
print()
32+
exit()
33+
except Exception:
34+
traceback.print_exc(file=sys.stdout)
35+
36+
37+
def valid(code, mode):
38+
try:
39+
ast.parse(code, mode=mode)
40+
return True
41+
except SyntaxError:
42+
return False
43+
44+
45+
if __name__ == "__main__":
46+
main()

0 commit comments

Comments
 (0)
0