Skip to content

Commit

Permalink
Merge pull request #563 from realpython/structural-pattern-matching
Browse files Browse the repository at this point in the history
Materials for Structural Pattern Matching: Initial Commit
  • Loading branch information
brendaweles authored Sep 20, 2024
2 parents 46e9df9 + 3814d95 commit ab4f4dc
Show file tree
Hide file tree
Showing 9 changed files with 476 additions and 0 deletions.
18 changes: 18 additions & 0 deletions structural-pattern-matching/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Structural Pattern Matching in Python

This folder contains the code samples for the Real Python tutorial [Structural Pattern Matching in Python](https://realpython.com/structural-pattern-matching/).

## Installation

Create and activate a virtual environment:

```sh
$ python -m venv venv/
$ source venv/bin/activate
```

Install the required third-party dependencies:

```sh
(venv) $ python -m pip install -r requirements.txt
```
37 changes: 37 additions & 0 deletions structural-pattern-matching/fetcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
from sys import stderr
from urllib.parse import ParseResult, urlparse


def fetch(url):
print(f"Fetching URL: {url}", file=stderr)
connection = make_connection(url)
try:
connection.request("GET", "/")
match connection.getresponse():
case HTTPResponse(status=code) if code >= 400:
raise ValueError("Failed to fetch URL")
case HTTPResponse(status=code) as resp if (
code >= 300 and (redirect := resp.getheader("Location"))
):
return fetch(redirect)
case HTTPResponse(status=code) as resp if code >= 200:
return resp.read()
case _:
raise ValueError("Unexpected response")
finally:
connection.close()


def make_connection(url):
match urlparse(url):
case ParseResult("http", netloc=host):
return HTTPConnection(host)
case ParseResult("https", netloc=host):
return HTTPSConnection(host)
case _:
raise ValueError("Unsupported URL scheme")


if __name__ == "__main__":
fetch("http://realpython.com/")
66 changes: 66 additions & 0 deletions structural-pattern-matching/guessing_game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import random

MIN, MAX = 1, 100
MAX_TRIES = 5
PROMPT_1 = f"\N{mage} Guess a number between {MIN} and {MAX}: "
PROMPT_2 = "\N{mage} Try again: "
BYE = "Bye \N{waving hand sign}"


def main():
print("Welcome to the game! Type 'q' or 'quit' to exit.")
while True:
play_game()
if not want_again():
bye()


def play_game():
drawn_number = random.randint(MIN, MAX)
num_tries = MAX_TRIES
prompt = PROMPT_1
while num_tries > 0:
match input(prompt):
case command if command.lower() in ("q", "quit"):
bye()
case user_input:
try:
user_number = int(user_input)
except ValueError:
print("That's not a number!")
else:
match user_number:
case number if number < drawn_number:
num_tries -= 1
prompt = PROMPT_2
print(f"Too low! {num_tries} tries left.")
case number if number > drawn_number:
num_tries -= 1
prompt = PROMPT_2
print(f"Too high! {num_tries} tries left.")
case _:
print("You won \N{party popper}")
return
print("You lost \N{pensive face}")


def want_again():
while True:
match input("Do you want to play again? [Y/N] ").lower():
case "y":
return True
case "n":
return False


def bye():
print(BYE)
exit()


if __name__ == "__main__":
try:
main()
except (KeyboardInterrupt, EOFError):
print()
bye()
41 changes: 41 additions & 0 deletions structural-pattern-matching/interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import sys


def interpret(code, num_bytes=2**10):
stack, brackets = [], {}
for i, instruction in enumerate(code):
match instruction:
case "[":
stack.append(i)
case "]":
brackets[i], brackets[j] = (j := stack.pop()), i
memory = bytearray(num_bytes)
pointer = ip = 0
while ip < len(code):
match code[ip]:
case ">":
pointer += 1
case "<":
pointer -= 1
case "+":
memory[pointer] += 1
case "-":
memory[pointer] -= 1
case ".":
print(chr(memory[pointer]), end="")
case ",":
memory[pointer] = ord(sys.stdin.buffer.read(1))
case "[" if memory[pointer] == 0:
ip = brackets[ip]
case "]" if memory[pointer] != 0:
ip = brackets[ip]
ip += 1


if __name__ == "__main__":
interpret(
"""
+++++++++++[>++++++>+++++++++>++++++++>++++>+++>+<<<<<<-]>+++
+++.>++.+++++++..+++.>>.>-.<<-.<.+++.------.--------.>>>+.>-.
"""
)
77 changes: 77 additions & 0 deletions structural-pattern-matching/issue_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import json
import urllib.request
from dataclasses import dataclass

from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel


@dataclass
class Comment:
when: str
body: str
url: str
user: str
user_url: str
issue_title: str

@property
def footer(self):
return (
f"Comment by [{self.user}]({self.user_url})"
f" on [{self.when}]({self.url})"
)

def render(self):
return Panel(
Markdown(f"{self.body}\n\n---\n_{self.footer}_"),
title=self.issue_title,
padding=1,
)


def fetch_github_events(org, repo):
url = f"https://api.github.com/repos/{org}/{repo}/events"
with urllib.request.urlopen(url) as response:
return json.loads(response.read())


def filter_comments(events):
for event in events:
match event:
case {
"type": "IssueCommentEvent",
"created_at": when,
"actor": {
"display_login": user,
},
"payload": {
"action": "created",
"issue": {
"state": "open",
"title": issue_title,
},
"comment": {
"body": body,
"html_url": url,
"user": {
"html_url": user_url,
},
},
},
}:
yield Comment(when, body, url, user, user_url, issue_title)


def main():
console = Console()
events = fetch_github_events("python", "cpython")
for comment in filter_comments(events):
console.clear()
console.print(comment.render())
console.input("\nPress [b]ENTER[/b] for the next comment...")


if __name__ == "__main__":
main()
57 changes: 57 additions & 0 deletions structural-pattern-matching/optimizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import ast
import inspect
import textwrap


def main():
source_code = inspect.getsource(sample_function)
source_tree = ast.parse(source_code)
target_tree = optimize(source_tree)
target_code = ast.unparse(target_tree)

print("Before:")
print(ast.dump(source_tree))
print(textwrap.indent(source_code, "| "))

print("After:")
print(ast.dump(target_tree))
print(textwrap.indent(target_code, "| "))


def sample_function():
return 40 + 2


def optimize(node):
match node:
case ast.Module(body, type_ignores):
return ast.Module(
[optimize(child) for child in body], type_ignores
)
case ast.FunctionDef():
return ast.FunctionDef(
name=node.name,
args=node.args,
body=[optimize(child) for child in node.body],
decorator_list=node.decorator_list,
returns=node.returns,
type_comment=node.type_comment,
type_params=node.type_params,
lineno=node.lineno,
)
case ast.Return(value):
return ast.Return(value=optimize(value))
case ast.BinOp(ast.Constant(left), op, ast.Constant(right)):
match op:
case ast.Add():
return ast.Constant(left + right)
case ast.Sub():
return ast.Constant(left - right)
case _:
return node
case _:
return node


if __name__ == "__main__":
main()
46 changes: 46 additions & 0 deletions structural-pattern-matching/repl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import ast
import sys
import traceback

PROMPT = "\N{snake} "
COMMANDS = ("help", "exit", "quit")


def main():
print('Type "help" for more information, "exit" or "quit" to finish.')
while True:
try:
match input(PROMPT):
case command if command.lower() in COMMANDS:
match command.lower():
case "help":
print(f"Python {sys.version}")
case "exit" | "quit":
break
case expression if valid(expression, "eval"):
_ = eval(expression)
if _ is not None:
print(_)
case statement if valid(statement, "exec"):
exec(statement)
case _:
print("Please type a command or valid Python")
except KeyboardInterrupt:
print("\nKeyboardInterrupt")
except EOFError:
print()
exit()
except Exception:
traceback.print_exc(file=sys.stdout)


def valid(code, mode):
try:
ast.parse(code, mode=mode)
return True
except SyntaxError:
return False


if __name__ == "__main__":
main()
Loading

0 comments on commit ab4f4dc

Please sign in to comment.