From df874a42ebcfefa2b84a875469a4806eb9c402bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Tue, 6 Aug 2024 15:24:52 +0200 Subject: [PATCH 01/29] Materials for Structural Pattern Matching: Initial Commit --- structural-pattern-matching/README.md | 18 +++ structural-pattern-matching/fetcher.py | 37 +++++ structural-pattern-matching/guessing_game.py | 65 +++++++++ structural-pattern-matching/interpreter.py | 41 ++++++ structural-pattern-matching/issue_comments.py | 77 +++++++++++ structural-pattern-matching/optimizer.py | 55 ++++++++ structural-pattern-matching/repl.py | 46 +++++++ structural-pattern-matching/repl_enhanced.py | 130 ++++++++++++++++++ structural-pattern-matching/requirements.txt | 4 + 9 files changed, 473 insertions(+) create mode 100644 structural-pattern-matching/README.md create mode 100644 structural-pattern-matching/fetcher.py create mode 100644 structural-pattern-matching/guessing_game.py create mode 100644 structural-pattern-matching/interpreter.py create mode 100644 structural-pattern-matching/issue_comments.py create mode 100644 structural-pattern-matching/optimizer.py create mode 100644 structural-pattern-matching/repl.py create mode 100644 structural-pattern-matching/repl_enhanced.py create mode 100644 structural-pattern-matching/requirements.txt diff --git a/structural-pattern-matching/README.md b/structural-pattern-matching/README.md new file mode 100644 index 0000000000..9595e7ad91 --- /dev/null +++ b/structural-pattern-matching/README.md @@ -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 +``` diff --git a/structural-pattern-matching/fetcher.py b/structural-pattern-matching/fetcher.py new file mode 100644 index 0000000000..98d21ce9db --- /dev/null +++ b/structural-pattern-matching/fetcher.py @@ -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/") diff --git a/structural-pattern-matching/guessing_game.py b/structural-pattern-matching/guessing_game.py new file mode 100644 index 0000000000..83530278b3 --- /dev/null +++ b/structural-pattern-matching/guessing_game.py @@ -0,0 +1,65 @@ +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(f"\n{BYE}") diff --git a/structural-pattern-matching/interpreter.py b/structural-pattern-matching/interpreter.py new file mode 100644 index 0000000000..609e61a2fe --- /dev/null +++ b/structural-pattern-matching/interpreter.py @@ -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( + """ + +++++++++++[>++++++>+++++++++>++++++++>++++>+++>+<<<<<<-]>+++ + +++.>++.+++++++..+++.>>.>-.<<-.<.+++.------.--------.>>>+.>-. + """ + ) diff --git a/structural-pattern-matching/issue_comments.py b/structural-pattern-matching/issue_comments.py new file mode 100644 index 0000000000..ebc6a622ad --- /dev/null +++ b/structural-pattern-matching/issue_comments.py @@ -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() diff --git a/structural-pattern-matching/optimizer.py b/structural-pattern-matching/optimizer.py new file mode 100644 index 0000000000..8074e84206 --- /dev/null +++ b/structural-pattern-matching/optimizer.py @@ -0,0 +1,55 @@ +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() diff --git a/structural-pattern-matching/repl.py b/structural-pattern-matching/repl.py new file mode 100644 index 0000000000..d15c3750ae --- /dev/null +++ b/structural-pattern-matching/repl.py @@ -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: + 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() diff --git a/structural-pattern-matching/repl_enhanced.py b/structural-pattern-matching/repl_enhanced.py new file mode 100644 index 0000000000..b4cf383b08 --- /dev/null +++ b/structural-pattern-matching/repl_enhanced.py @@ -0,0 +1,130 @@ +import ast +import atexit +import readline +import rlcompleter +import sys +import traceback +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal + +STANDARD_PROMPT = ">>> " +INDENTED_PROMPT = "... " +TAB_WIDTH = 4 +TAB = TAB_WIDTH * " " +COMMANDS = ("help", "exit", "quit") +PYTHON_HISTORY = Path.home() / ".python_history" + + +@dataclass +class Console: + indentation_level: int = 0 + + def __post_init__(self) -> None: + readline.parse_and_bind("tab: complete") + readline.set_completer(rlcompleter.Completer().complete) + if PYTHON_HISTORY.exists(): + readline.read_history_file(PYTHON_HISTORY) + atexit.register(readline.write_history_file, PYTHON_HISTORY) + + @property + def prompt(self) -> str: + if self.indentation_level > 0: + return INDENTED_PROMPT + else: + return STANDARD_PROMPT + + @property + def indentation(self) -> str: + return TAB * self.indentation_level + + def indent(self) -> None: + self.indentation_level += 1 + + def dedent(self) -> None: + if self.indentation_level > 0: + self.indentation_level -= 1 + + def reindent(self, line: str) -> None: + num_leading_spaces = len(line) - len(line.lstrip()) + new_indentation_level = num_leading_spaces // TAB_WIDTH + if new_indentation_level < self.indentation_level: + self.indentation_level = new_indentation_level + + def input(self) -> str: + def hook(): + readline.insert_text(self.indentation) + readline.redisplay() + + try: + readline.set_pre_input_hook(hook) + result = input(self.prompt) + return result + finally: + readline.set_pre_input_hook() + + +@dataclass +class CodeBlock: + lines: list[str] = field(default_factory=list) + + def execute(self) -> None: + exec("\n".join(self.lines), globals()) + self.lines = [] + + +def main() -> None: + print('Type "help" for more information, "exit" or "quit" to finish.') + console = Console() + block = CodeBlock() + while True: + try: + match console.input(): + case command if command.lower() in COMMANDS: + match command.lower(): + case "help": + print(f"Python {sys.version}") + case "exit" | "quit": + break + case line if line.endswith(":"): + block.lines.append(line) + console.reindent(line) + console.indent() + case line if line.lstrip() == "": + console.reindent(line) + console.dedent() + if console.indentation_level == 0 and block.lines: + block.execute() + case line if console.indentation_level > 0: + block.lines.append(line) + 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") + console.indentation_level = 0 + block.lines = [] + except EOFError: + print() + sys.exit() + except: + traceback.print_exc(file=sys.stdout) + console.indentation_level = 0 + block.lines = [] + + +def valid(code: str, mode: Literal["eval", "exec"]) -> bool: + try: + ast.parse(code, mode=mode) + return True + except SyntaxError: + return False + + +if __name__ == "__main__": + main() diff --git a/structural-pattern-matching/requirements.txt b/structural-pattern-matching/requirements.txt new file mode 100644 index 0000000000..ffba812892 --- /dev/null +++ b/structural-pattern-matching/requirements.txt @@ -0,0 +1,4 @@ +markdown-it-py==3.0.0 +mdurl==0.1.2 +Pygments==2.18.0 +rich==13.7.1 From 63ce65cebbe7cbda71f34d07d939019ac9b81415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Tue, 6 Aug 2024 15:27:02 +0200 Subject: [PATCH 02/29] Add noqa to silence the linter --- structural-pattern-matching/repl.py | 2 +- structural-pattern-matching/repl_enhanced.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/structural-pattern-matching/repl.py b/structural-pattern-matching/repl.py index d15c3750ae..7c6bbae8fc 100644 --- a/structural-pattern-matching/repl.py +++ b/structural-pattern-matching/repl.py @@ -30,7 +30,7 @@ def main(): except EOFError: print() exit() - except: + except: # noqa traceback.print_exc(file=sys.stdout) diff --git a/structural-pattern-matching/repl_enhanced.py b/structural-pattern-matching/repl_enhanced.py index b4cf383b08..079a431d19 100644 --- a/structural-pattern-matching/repl_enhanced.py +++ b/structural-pattern-matching/repl_enhanced.py @@ -112,7 +112,7 @@ def main() -> None: except EOFError: print() sys.exit() - except: + except: # noqa traceback.print_exc(file=sys.stdout) console.indentation_level = 0 block.lines = [] From 7ffbbfc4c4b4e89b34bdd100dd47ea988de6d6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Tue, 6 Aug 2024 15:28:34 +0200 Subject: [PATCH 03/29] Reformat code --- structural-pattern-matching/optimizer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/structural-pattern-matching/optimizer.py b/structural-pattern-matching/optimizer.py index 8074e84206..431e3c0307 100644 --- a/structural-pattern-matching/optimizer.py +++ b/structural-pattern-matching/optimizer.py @@ -25,7 +25,9 @@ def sample_function(): def optimize(node): match node: case ast.Module(body, type_ignores): - return ast.Module([optimize(child) for child in body], type_ignores) + return ast.Module( + [optimize(child) for child in body], type_ignores + ) case ast.FunctionDef(): return ast.FunctionDef( name=node.name, From 76bad3832342fa3f360d74d44a97068192005975 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 30 Aug 2024 16:34:14 +0200 Subject: [PATCH 04/29] Sample code for the article on building a contact book with Textual --- .../source_code/README.md | 33 ++++ .../source_code/requirements.txt | 1 + .../source_code/rpcontacts/__init__.py | 1 + .../source_code/rpcontacts/__main__.py | 11 ++ .../source_code/rpcontacts/database.py | 52 ++++++ .../source_code/rpcontacts/rpcontacts.tcss | 75 +++++++++ .../source_code/rpcontacts/tui.py | 156 ++++++++++++++++++ .../source_code_step_1/README.md | 34 ++++ .../source_code_step_1/requirements.txt | 2 + .../source_code_step_1/rpcontacts/__init__.py | 1 + .../source_code_step_1/rpcontacts/__main__.py | 10 ++ .../source_code_step_1/rpcontacts/database.py | 0 .../rpcontacts/rpcontacts.tcss | 25 +++ .../source_code_step_1/rpcontacts/tui.py | 53 ++++++ .../source_code_step_2/README.md | 34 ++++ .../source_code_step_2/requirements.txt | 1 + .../source_code_step_2/rpcontacts/__init__.py | 1 + .../source_code_step_2/rpcontacts/__main__.py | 10 ++ .../source_code_step_2/rpcontacts/database.py | 0 .../rpcontacts/rpcontacts.tcss | 42 +++++ .../source_code_step_2/rpcontacts/tui.py | 71 ++++++++ .../source_code_step_3/README.md | 34 ++++ .../source_code_step_3/requirements.txt | 1 + .../source_code_step_3/rpcontacts/__init__.py | 1 + .../source_code_step_3/rpcontacts/__main__.py | 10 ++ .../source_code_step_3/rpcontacts/database.py | 61 +++++++ .../rpcontacts/rpcontacts.tcss | 42 +++++ .../source_code_step_3/rpcontacts/tui.py | 71 ++++++++ .../source_code_step_4/README.md | 34 ++++ .../source_code_step_4/requirements.txt | 1 + .../source_code_step_4/rpcontacts/__init__.py | 1 + .../source_code_step_4/rpcontacts/__main__.py | 11 ++ .../source_code_step_4/rpcontacts/database.py | 61 +++++++ .../rpcontacts/rpcontacts.tcss | 42 +++++ .../source_code_step_4/rpcontacts/tui.py | 82 +++++++++ .../source_code_step_5/README.md | 34 ++++ .../source_code_step_5/requirements.txt | 1 + .../source_code_step_5/rpcontacts/__init__.py | 1 + .../source_code_step_5/rpcontacts/__main__.py | 11 ++ .../source_code_step_5/rpcontacts/database.py | 61 +++++++ .../rpcontacts/rpcontacts.tcss | 75 +++++++++ .../source_code_step_5/rpcontacts/tui.py | 126 ++++++++++++++ .../source_code_step_6/README.md | 33 ++++ .../source_code_step_6/requirements.txt | 1 + .../source_code_step_6/rpcontacts/__init__.py | 1 + .../source_code_step_6/rpcontacts/__main__.py | 11 ++ .../source_code_step_6/rpcontacts/database.py | 52 ++++++ .../rpcontacts/rpcontacts.tcss | 75 +++++++++ .../source_code_step_6/rpcontacts/tui.py | 156 ++++++++++++++++++ 49 files changed, 1703 insertions(+) create mode 100755 contact-book-python-textual/source_code/README.md create mode 100755 contact-book-python-textual/source_code/requirements.txt create mode 100755 contact-book-python-textual/source_code/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_1/README.md create mode 100755 contact-book-python-textual/source_code_step_1/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_2/README.md create mode 100755 contact-book-python-textual/source_code_step_2/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_3/README.md create mode 100755 contact-book-python-textual/source_code_step_3/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_4/README.md create mode 100755 contact-book-python-textual/source_code_step_4/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_5/README.md create mode 100755 contact-book-python-textual/source_code_step_5/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_6/README.md create mode 100755 contact-book-python-textual/source_code_step_6/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/tui.py diff --git a/contact-book-python-textual/source_code/README.md b/contact-book-python-textual/source_code/README.md new file mode 100755 index 0000000000..0058b1886a --- /dev/null +++ b/contact-book-python-textual/source_code/README.md @@ -0,0 +1,33 @@ +# RP Contacts + +**RP Contacts** is a contact book application built with Python, Textual, and SQLite. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the project's requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. diff --git a/contact-book-python-textual/source_code/requirements.txt b/contact-book-python-textual/source_code/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code/rpcontacts/__init__.py b/contact-book-python-textual/source_code/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code/rpcontacts/__main__.py b/contact-book-python-textual/source_code/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code/rpcontacts/database.py b/contact-book-python-textual/source_code/rpcontacts/database.py new file mode 100644 index 0000000000..c88dacbf98 --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/database.py @@ -0,0 +1,52 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") diff --git a/contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..02ea7df5c4 --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss @@ -0,0 +1,75 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} + +InputDialog { + align: center middle; +} + +#title { + column-span: 3; + height: 1fr; + width: 1fr; + content-align: center middle; + color: green; + text-style: bold; +} + +#input-dialog { + grid-size: 3 5; + grid-gutter: 1 1; + padding: 0 1; + width: 50; + height: 20; + border: solid green; + background: $surface; +} + +.label { + height: 1fr; + width: 1fr; + content-align: right middle; +} + +.input { + column-span: 2; +} diff --git a/contact-book-python-textual/source_code/rpcontacts/tui.py b/contact-book-python-textual/source_code/rpcontacts/tui.py new file mode 100644 index 0000000000..9c08587f5d --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/tui.py @@ -0,0 +1,156 @@ +from textual.app import App, on +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import ( + Button, + DataTable, + Footer, + Header, + Input, + Label, + Static, +) + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + @on(Button.Pressed, "#add") + def action_add(self): + def check_contact(contact_data): + if contact_data: + self.db.add_contact(contact_data) + id, *contact = self.db.get_last_contact() + self.query_one(DataTable).add_row(*contact, key=id) + + self.push_screen(InputDialog(), check_contact) + + @on(Button.Pressed, "#delete") + def action_delete(self): + contacts_list = self.query_one(DataTable) + row_key, _ = contacts_list.coordinate_to_cell_key( + contacts_list.cursor_coordinate + ) + + def check_answer(accepted): + if accepted and row_key: + self.db.delete_contact(id=row_key.value) + contacts_list.remove_row(row_key) + + name = contacts_list.get_row(row_key)[0] + self.push_screen( + QuestionDialog(f"Do you want to delete {name}'s contact?"), + check_answer, + ) + + @on(Button.Pressed, "#clear") + def action_clear_all(self): + def check_answer(accepted): + if accepted: + self.db.clear_all_contacts() + self.query_one(DataTable).clear() + + self.push_screen( + QuestionDialog("Are you sure you want te remove all contacts?"), + check_answer, + ) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) + + +class InputDialog(Screen): + def compose(self): + yield Grid( + Label("Add Contact", id="title"), + Label("Name:", classes="label"), + Input(placeholder="Contact Name", classes="input", id="name"), + Label("Phone:", classes="label"), + Input(placeholder="Contact Phone", classes="input", id="phone"), + Label("Email:", classes="label"), + Input(placeholder="Contact Email", classes="input", id="email"), + Static(), + Button("Cancel", variant="warning", id="cancel"), + Button("Ok", variant="success", id="ok"), + id="input-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "ok": + name = self.query_one("#name", Input).value + phone = self.query_one("#phone", Input).value + email = self.query_one("#email", Input).value + self.dismiss((name, phone, email)) + else: + self.dismiss(()) diff --git a/contact-book-python-textual/source_code_step_1/README.md b/contact-book-python-textual/source_code_step_1/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_1/requirements.txt b/contact-book-python-textual/source_code_step_1/requirements.txt new file mode 100755 index 0000000000..99f67d03a3 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/requirements.txt @@ -0,0 +1,2 @@ +textual==0.75.1 +textual-dev==1.5.1 diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py new file mode 100644 index 0000000000..6fbc6f4277 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py @@ -0,0 +1,10 @@ +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/database.py b/contact-book-python-textual/source_code_step_1/rpcontacts/database.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..9349b5ff46 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss @@ -0,0 +1,25 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_1/rpcontacts/tui.py new file mode 100644 index 0000000000..2a0e155918 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/tui.py @@ -0,0 +1,53 @@ +from textual.app import App +from textual.containers import Grid +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Label + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("q", "request_quit", "Quit"), + ] + + def compose(self): + yield Header() + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_2/README.md b/contact-book-python-textual/source_code_step_2/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_2/requirements.txt b/contact-book-python-textual/source_code_step_2/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py new file mode 100644 index 0000000000..6fbc6f4277 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py @@ -0,0 +1,10 @@ +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/database.py b/contact-book-python-textual/source_code_step_2/rpcontacts/database.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..29d34d8a70 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss @@ -0,0 +1,42 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py new file mode 100644 index 0000000000..14e5a01597 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py @@ -0,0 +1,71 @@ +from textual.app import App +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, DataTable, Footer, Header, Label, Static + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_3/README.md b/contact-book-python-textual/source_code_step_3/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_3/requirements.txt b/contact-book-python-textual/source_code_step_3/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py new file mode 100644 index 0000000000..6fbc6f4277 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py @@ -0,0 +1,10 @@ +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/database.py b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py new file mode 100644 index 0000000000..accbb77c47 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py @@ -0,0 +1,61 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") + + +data = [ + ("Linda", "111-2222-3333", "linda@example.com"), + ("Joe", "111-2222-3333", "joe@example.com"), + ("Lara", "111-2222-3333", "lara@example.com"), + ("David", "111-2222-3333", "david@example.com"), + ("Jane", "111-2222-3333", "jane@example.com"), +] diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..29d34d8a70 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss @@ -0,0 +1,42 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py new file mode 100644 index 0000000000..14e5a01597 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py @@ -0,0 +1,71 @@ +from textual.app import App +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, DataTable, Footer, Header, Label, Static + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_4/README.md b/contact-book-python-textual/source_code_step_4/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_4/requirements.txt b/contact-book-python-textual/source_code_step_4/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/database.py b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py new file mode 100644 index 0000000000..accbb77c47 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py @@ -0,0 +1,61 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") + + +data = [ + ("Linda", "111-2222-3333", "linda@example.com"), + ("Joe", "111-2222-3333", "joe@example.com"), + ("Lara", "111-2222-3333", "lara@example.com"), + ("David", "111-2222-3333", "david@example.com"), + ("Jane", "111-2222-3333", "jane@example.com"), +] diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..29d34d8a70 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss @@ -0,0 +1,42 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py new file mode 100644 index 0000000000..b0b441cb5a --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py @@ -0,0 +1,82 @@ +from textual.app import App +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, DataTable, Footer, Header, Label, Static + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_5/README.md b/contact-book-python-textual/source_code_step_5/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_5/requirements.txt b/contact-book-python-textual/source_code_step_5/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/database.py b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py new file mode 100644 index 0000000000..accbb77c47 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py @@ -0,0 +1,61 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") + + +data = [ + ("Linda", "111-2222-3333", "linda@example.com"), + ("Joe", "111-2222-3333", "joe@example.com"), + ("Lara", "111-2222-3333", "lara@example.com"), + ("David", "111-2222-3333", "david@example.com"), + ("Jane", "111-2222-3333", "jane@example.com"), +] diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..02ea7df5c4 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss @@ -0,0 +1,75 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} + +InputDialog { + align: center middle; +} + +#title { + column-span: 3; + height: 1fr; + width: 1fr; + content-align: center middle; + color: green; + text-style: bold; +} + +#input-dialog { + grid-size: 3 5; + grid-gutter: 1 1; + padding: 0 1; + width: 50; + height: 20; + border: solid green; + background: $surface; +} + +.label { + height: 1fr; + width: 1fr; + content-align: right middle; +} + +.input { + column-span: 2; +} diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py new file mode 100644 index 0000000000..bd69c739cc --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py @@ -0,0 +1,126 @@ +from textual.app import App, on +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import ( + Button, + DataTable, + Footer, + Header, + Input, + Label, + Static, +) + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + @on(Button.Pressed, "#add") + def action_add(self): + def check_contact(contact_data): + if contact_data: + self.db.add_contact(contact_data) + id, *contact = self.db.get_last_contact() + self.query_one(DataTable).add_row(*contact, key=id) + + self.push_screen(InputDialog(), check_contact) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) + + +class InputDialog(Screen): + def compose(self): + yield Grid( + Label("Add Contact", id="title"), + Label("Name:", classes="label"), + Input(placeholder="Contact Name", classes="input", id="name"), + Label("Phone:", classes="label"), + Input(placeholder="Contact Phone", classes="input", id="phone"), + Label("Email:", classes="label"), + Input(placeholder="Contact Email", classes="input", id="email"), + Static(), + Button("Cancel", variant="warning", id="cancel"), + Button("Ok", variant="success", id="ok"), + id="input-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "ok": + name = self.query_one("#name", Input).value + phone = self.query_one("#phone", Input).value + email = self.query_one("#email", Input).value + self.dismiss((name, phone, email)) + else: + self.dismiss(()) diff --git a/contact-book-python-textual/source_code_step_6/README.md b/contact-book-python-textual/source_code_step_6/README.md new file mode 100755 index 0000000000..0058b1886a --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/README.md @@ -0,0 +1,33 @@ +# RP Contacts + +**RP Contacts** is a contact book application built with Python, Textual, and SQLite. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the project's requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. diff --git a/contact-book-python-textual/source_code_step_6/requirements.txt b/contact-book-python-textual/source_code_step_6/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/database.py b/contact-book-python-textual/source_code_step_6/rpcontacts/database.py new file mode 100644 index 0000000000..c88dacbf98 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/database.py @@ -0,0 +1,52 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..02ea7df5c4 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss @@ -0,0 +1,75 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} + +InputDialog { + align: center middle; +} + +#title { + column-span: 3; + height: 1fr; + width: 1fr; + content-align: center middle; + color: green; + text-style: bold; +} + +#input-dialog { + grid-size: 3 5; + grid-gutter: 1 1; + padding: 0 1; + width: 50; + height: 20; + border: solid green; + background: $surface; +} + +.label { + height: 1fr; + width: 1fr; + content-align: right middle; +} + +.input { + column-span: 2; +} diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py new file mode 100644 index 0000000000..9c08587f5d --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py @@ -0,0 +1,156 @@ +from textual.app import App, on +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import ( + Button, + DataTable, + Footer, + Header, + Input, + Label, + Static, +) + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + @on(Button.Pressed, "#add") + def action_add(self): + def check_contact(contact_data): + if contact_data: + self.db.add_contact(contact_data) + id, *contact = self.db.get_last_contact() + self.query_one(DataTable).add_row(*contact, key=id) + + self.push_screen(InputDialog(), check_contact) + + @on(Button.Pressed, "#delete") + def action_delete(self): + contacts_list = self.query_one(DataTable) + row_key, _ = contacts_list.coordinate_to_cell_key( + contacts_list.cursor_coordinate + ) + + def check_answer(accepted): + if accepted and row_key: + self.db.delete_contact(id=row_key.value) + contacts_list.remove_row(row_key) + + name = contacts_list.get_row(row_key)[0] + self.push_screen( + QuestionDialog(f"Do you want to delete {name}'s contact?"), + check_answer, + ) + + @on(Button.Pressed, "#clear") + def action_clear_all(self): + def check_answer(accepted): + if accepted: + self.db.clear_all_contacts() + self.query_one(DataTable).clear() + + self.push_screen( + QuestionDialog("Are you sure you want te remove all contacts?"), + check_answer, + ) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) + + +class InputDialog(Screen): + def compose(self): + yield Grid( + Label("Add Contact", id="title"), + Label("Name:", classes="label"), + Input(placeholder="Contact Name", classes="input", id="name"), + Label("Phone:", classes="label"), + Input(placeholder="Contact Phone", classes="input", id="phone"), + Label("Email:", classes="label"), + Input(placeholder="Contact Email", classes="input", id="email"), + Static(), + Button("Cancel", variant="warning", id="cancel"), + Button("Ok", variant="success", id="ok"), + id="input-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "ok": + name = self.query_one("#name", Input).value + phone = self.query_one("#phone", Input).value + email = self.query_one("#email", Input).value + self.dismiss((name, phone, email)) + else: + self.dismiss(()) From 73606334d9071b52fe26576543649a2e5fec42fe Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 30 Aug 2024 16:36:39 +0200 Subject: [PATCH 05/29] Copy the `README.md` file to the root --- contact-book-python-textual/README.md | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 contact-book-python-textual/README.md diff --git a/contact-book-python-textual/README.md b/contact-book-python-textual/README.md new file mode 100755 index 0000000000..0058b1886a --- /dev/null +++ b/contact-book-python-textual/README.md @@ -0,0 +1,33 @@ +# RP Contacts + +**RP Contacts** is a contact book application built with Python, Textual, and SQLite. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the project's requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. From 2aa19cc68bdd13a9f3242612f2235e840b62c496 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 30 Aug 2024 16:38:43 +0200 Subject: [PATCH 06/29] Fix the `README.md` --- contact-book-python-textual/README.md | 34 ++------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/contact-book-python-textual/README.md b/contact-book-python-textual/README.md index 0058b1886a..36ef850a2f 100755 --- a/contact-book-python-textual/README.md +++ b/contact-book-python-textual/README.md @@ -1,33 +1,3 @@ -# RP Contacts +# Build a Contact Book App With Python, Textual, and SQLite -**RP Contacts** is a contact book application built with Python, Textual, and SQLite. - -## Installation - -1. Create a Python virtual environment - -```sh -$ python -m venv ./venv -$ source venv/bin/activate -(venv) $ -``` - -2. Install the project's requirements - -```sh -(venv) $ python -m pip install -r requirements.txt -``` - -## Run the Project - -```sh -(venv) $ python -m rpcontacts -``` - -## About the Author - -Real Python - Email: office@realpython.com - -## License - -Distributed under the MIT license. See `LICENSE` for more information. +This folder provides the code examples for the Real Python tutorial [Build a Contact Book App With Python, Textual, and SQLite](https://realpython.com/contact-book-python-textual/). \ No newline at end of file From 7d23cd3d2db82aa5ad18a51ec3d136816f2508ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Thu, 5 Sep 2024 12:36:21 +0200 Subject: [PATCH 07/29] [PATMA] TR Feedback --- structural-pattern-matching/guessing_game.py | 3 ++- structural-pattern-matching/repl.py | 2 +- structural-pattern-matching/repl_enhanced.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/structural-pattern-matching/guessing_game.py b/structural-pattern-matching/guessing_game.py index 83530278b3..333524320c 100644 --- a/structural-pattern-matching/guessing_game.py +++ b/structural-pattern-matching/guessing_game.py @@ -62,4 +62,5 @@ def bye(): try: main() except (KeyboardInterrupt, EOFError): - print(f"\n{BYE}") + print() + bye() diff --git a/structural-pattern-matching/repl.py b/structural-pattern-matching/repl.py index 7c6bbae8fc..7e4bb0e823 100644 --- a/structural-pattern-matching/repl.py +++ b/structural-pattern-matching/repl.py @@ -30,7 +30,7 @@ def main(): except EOFError: print() exit() - except: # noqa + except Exception: traceback.print_exc(file=sys.stdout) diff --git a/structural-pattern-matching/repl_enhanced.py b/structural-pattern-matching/repl_enhanced.py index 079a431d19..b5d5dda232 100644 --- a/structural-pattern-matching/repl_enhanced.py +++ b/structural-pattern-matching/repl_enhanced.py @@ -112,7 +112,7 @@ def main() -> None: except EOFError: print() sys.exit() - except: # noqa + except Exception: traceback.print_exc(file=sys.stdout) console.indentation_level = 0 block.lines = [] From 4e7617bda06cc8951bd7edf34afae30a3080b4f8 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 6 Sep 2024 16:44:38 +0200 Subject: [PATCH 08/29] Sample code for the article on syntactic sugar --- syntactic-sugar-python/README.md | 3 +++ syntactic-sugar-python/assertions.py | 9 +++++++++ syntactic-sugar-python/balance.py | 12 ++++++++++++ syntactic-sugar-python/circle.py | 18 ++++++++++++++++++ syntactic-sugar-python/membership.py | 5 +++++ syntactic-sugar-python/stack.py | 19 +++++++++++++++++++ syntactic-sugar-python/timing.py | 21 +++++++++++++++++++++ syntactic-sugar-python/user_input.py | 5 +++++ 8 files changed, 92 insertions(+) create mode 100644 syntactic-sugar-python/README.md create mode 100644 syntactic-sugar-python/assertions.py create mode 100644 syntactic-sugar-python/balance.py create mode 100644 syntactic-sugar-python/circle.py create mode 100644 syntactic-sugar-python/membership.py create mode 100644 syntactic-sugar-python/stack.py create mode 100644 syntactic-sugar-python/timing.py create mode 100644 syntactic-sugar-python/user_input.py diff --git a/syntactic-sugar-python/README.md b/syntactic-sugar-python/README.md new file mode 100644 index 0000000000..71228b1e5f --- /dev/null +++ b/syntactic-sugar-python/README.md @@ -0,0 +1,3 @@ +# Syntactic Sugar in Python: Make Your Code Pythonic + +This folder provides the code examples for the Real Python tutorial [Syntactic Sugar in Python: Make Your Code Pythonic](https://realpython.com/python-syntactic-sugar/). diff --git a/syntactic-sugar-python/assertions.py b/syntactic-sugar-python/assertions.py new file mode 100644 index 0000000000..a5a774dadd --- /dev/null +++ b/syntactic-sugar-python/assertions.py @@ -0,0 +1,9 @@ +number = 42 +if __debug__: + if not number > 0: + raise AssertionError("number must be positive") + +number = -42 +if __debug__: + if not number > 0: + raise AssertionError("number must be positive") diff --git a/syntactic-sugar-python/balance.py b/syntactic-sugar-python/balance.py new file mode 100644 index 0000000000..3e84d19133 --- /dev/null +++ b/syntactic-sugar-python/balance.py @@ -0,0 +1,12 @@ +debit = 300.00 +credit = 450.00 + +print( + f"Debit: ${debit:.2f}, Credit: ${credit:.2f}, Balance: ${credit - debit:.2f}" +) + +print( + "Debit: ${:.2f}, Credit: ${:.2f}, Balance: ${:.2f}".format( + debit, credit, credit - debit + ) +) diff --git a/syntactic-sugar-python/circle.py b/syntactic-sugar-python/circle.py new file mode 100644 index 0000000000..90baeceace --- /dev/null +++ b/syntactic-sugar-python/circle.py @@ -0,0 +1,18 @@ +from math import pi + + +class Circle: + def __init__(self, radius): + self.radius = radius + + def area(self): + return pi * self.radius**2 + + def perimeter(self): + return 2 * pi * self.radius + + +circle = Circle(10) +print(circle.radius) +print(circle.area()) +print(circle.perimeter()) diff --git a/syntactic-sugar-python/membership.py b/syntactic-sugar-python/membership.py new file mode 100644 index 0000000000..49af2d913f --- /dev/null +++ b/syntactic-sugar-python/membership.py @@ -0,0 +1,5 @@ +def is_member(value, iterable): + for current_value in iterable: + if current_value == value: + return True + return False diff --git a/syntactic-sugar-python/stack.py b/syntactic-sugar-python/stack.py new file mode 100644 index 0000000000..a551895da3 --- /dev/null +++ b/syntactic-sugar-python/stack.py @@ -0,0 +1,19 @@ +class Stack: + def __init__(self): + self.items = [] + + def push(self, item): + self.items.append(item) + + def pop(self): + return self.items.pop() + + def __iter__(self): + yield from self.items + + # def __iter__(self): + # return iter(self.items) + + # def __iter__(self): + # for item in self.items: + # yield item diff --git a/syntactic-sugar-python/timing.py b/syntactic-sugar-python/timing.py new file mode 100644 index 0000000000..52da01c230 --- /dev/null +++ b/syntactic-sugar-python/timing.py @@ -0,0 +1,21 @@ +import time + + +def timer(func): + def _timer(*args, **kwargs): + start = time.time() + result = func(*args, **kwargs) + end = time.time() + print(f"Execution time: {end - start:.4f} seconds") + return result + + return _timer + + +@timer +def delayed_mean(sample): + time.sleep(1) + return sum(sample) / len(sample) + + +delayed_mean([10, 2, 4, 7, 9, 3, 9, 8, 6, 7]) diff --git a/syntactic-sugar-python/user_input.py b/syntactic-sugar-python/user_input.py new file mode 100644 index 0000000000..1b0acd68ef --- /dev/null +++ b/syntactic-sugar-python/user_input.py @@ -0,0 +1,5 @@ +line = input("Type some text: ") + +while line != "stop": + print(line) + line = input("Type some text: ") From 4272b56fc0effcd7fd0bc493f8d2548d5926e862 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Mon, 9 Sep 2024 13:41:22 +0200 Subject: [PATCH 09/29] TR updates, first round --- .../source_code/rpcontacts/tui.py | 2 +- .../source_code_step_2/rpcontacts/tui.py | 2 +- .../source_code_step_3/rpcontacts/database.py | 9 --------- .../source_code_step_3/rpcontacts/tui.py | 2 +- .../source_code_step_4/rpcontacts/database.py | 9 --------- .../source_code_step_4/rpcontacts/tui.py | 2 +- .../source_code_step_5/rpcontacts/database.py | 9 --------- .../source_code_step_5/rpcontacts/tui.py | 2 +- .../source_code_step_6/rpcontacts/tui.py | 2 +- 9 files changed, 6 insertions(+), 33 deletions(-) diff --git a/contact-book-python-textual/source_code/rpcontacts/tui.py b/contact-book-python-textual/source_code/rpcontacts/tui.py index 9c08587f5d..fd46db97ad 100644 --- a/contact-book-python-textual/source_code/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code/rpcontacts/tui.py @@ -18,7 +18,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py index 14e5a01597..37b9757b9c 100644 --- a/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py @@ -10,7 +10,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/database.py b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py index accbb77c47..c88dacbf98 100644 --- a/contact-book-python-textual/source_code_step_3/rpcontacts/database.py +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py @@ -50,12 +50,3 @@ def delete_contact(self, id): def clear_all_contacts(self): self._run_query("DELETE FROM contacts;") - - -data = [ - ("Linda", "111-2222-3333", "linda@example.com"), - ("Joe", "111-2222-3333", "joe@example.com"), - ("Lara", "111-2222-3333", "lara@example.com"), - ("David", "111-2222-3333", "david@example.com"), - ("Jane", "111-2222-3333", "jane@example.com"), -] diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py index 14e5a01597..37b9757b9c 100644 --- a/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py @@ -10,7 +10,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/database.py b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py index accbb77c47..c88dacbf98 100644 --- a/contact-book-python-textual/source_code_step_4/rpcontacts/database.py +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py @@ -50,12 +50,3 @@ def delete_contact(self, id): def clear_all_contacts(self): self._run_query("DELETE FROM contacts;") - - -data = [ - ("Linda", "111-2222-3333", "linda@example.com"), - ("Joe", "111-2222-3333", "joe@example.com"), - ("Lara", "111-2222-3333", "lara@example.com"), - ("David", "111-2222-3333", "david@example.com"), - ("Jane", "111-2222-3333", "jane@example.com"), -] diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py index b0b441cb5a..3087ba2300 100644 --- a/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py @@ -10,7 +10,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/database.py b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py index accbb77c47..c88dacbf98 100644 --- a/contact-book-python-textual/source_code_step_5/rpcontacts/database.py +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py @@ -50,12 +50,3 @@ def delete_contact(self, id): def clear_all_contacts(self): self._run_query("DELETE FROM contacts;") - - -data = [ - ("Linda", "111-2222-3333", "linda@example.com"), - ("Joe", "111-2222-3333", "joe@example.com"), - ("Lara", "111-2222-3333", "lara@example.com"), - ("David", "111-2222-3333", "david@example.com"), - ("Jane", "111-2222-3333", "jane@example.com"), -] diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py index bd69c739cc..ab422bba01 100644 --- a/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py @@ -18,7 +18,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py index 9c08587f5d..fd46db97ad 100644 --- a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py @@ -18,7 +18,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] From a4086017c3b875648941a91c77b5e8da599a8f83 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 13 Sep 2024 18:37:01 +0200 Subject: [PATCH 10/29] Sample code for the article on closures --- python-closure/README.md | 3 +++ python-closure/app.py | 25 +++++++++++++++++++++++++ python-closure/appender.py | 8 ++++++++ python-closure/closure.py | 16 ++++++++++++++++ python-closure/counter.py | 9 +++++++++ python-closure/cum_average.py | 8 ++++++++ python-closure/decorators.py | 12 ++++++++++++ python-closure/free_variables.py | 11 +++++++++++ python-closure/inner.py | 7 +++++++ python-closure/memoization.py | 27 +++++++++++++++++++++++++++ python-closure/roots.py | 28 ++++++++++++++++++++++++++++ python-closure/stack_class.py | 12 ++++++++++++ python-closure/stack_closure.py | 15 +++++++++++++++ 13 files changed, 181 insertions(+) create mode 100644 python-closure/README.md create mode 100644 python-closure/app.py create mode 100644 python-closure/appender.py create mode 100644 python-closure/closure.py create mode 100644 python-closure/counter.py create mode 100644 python-closure/cum_average.py create mode 100644 python-closure/decorators.py create mode 100644 python-closure/free_variables.py create mode 100644 python-closure/inner.py create mode 100644 python-closure/memoization.py create mode 100644 python-closure/roots.py create mode 100644 python-closure/stack_class.py create mode 100644 python-closure/stack_closure.py diff --git a/python-closure/README.md b/python-closure/README.md new file mode 100644 index 0000000000..ce9c6018e3 --- /dev/null +++ b/python-closure/README.md @@ -0,0 +1,3 @@ +# Python Closures: Common Use Cases and Examples + +This folder provides the code examples for the Real Python tutorial [Python Closures: Common Use Cases and Examples](https://realpython.com/python-closure/). diff --git a/python-closure/app.py b/python-closure/app.py new file mode 100644 index 0000000000..c9368b3824 --- /dev/null +++ b/python-closure/app.py @@ -0,0 +1,25 @@ +import tkinter as tk + +app = tk.Tk() +app.title("GUI App") +app.geometry("200x60") + +label = tk.Label(app, font=("Helvetica", 16, "bold")) +label.pack() + + +def callback(text): + def closure(): + label.config(text=text) + + return closure + + +button = tk.Button( + app, + text="Greet", + command=callback("Hello, World!"), +) +button.pack() + +app.mainloop() diff --git a/python-closure/appender.py b/python-closure/appender.py new file mode 100644 index 0000000000..b179e52668 --- /dev/null +++ b/python-closure/appender.py @@ -0,0 +1,8 @@ +def make_appender(): + items = [] + + def appender(new_item): + items.append(new_item) + return items + + return appender diff --git a/python-closure/closure.py b/python-closure/closure.py new file mode 100644 index 0000000000..ce99e3f69e --- /dev/null +++ b/python-closure/closure.py @@ -0,0 +1,16 @@ +# def outer_func(): +# name = "Pythonista" + +# def inner_func(): +# print(f"Hello, {name}!") + +# return inner_func + + +def outer_func(): + name = "Pythonista" + return lambda name=name: print(f"Hello, {name}!") + + +inner_func = outer_func() +inner_func() diff --git a/python-closure/counter.py b/python-closure/counter.py new file mode 100644 index 0000000000..a2dac3d68e --- /dev/null +++ b/python-closure/counter.py @@ -0,0 +1,9 @@ +def make_counter(): + count = 0 + + def counter(): + nonlocal count + count += 1 + return count + + return counter diff --git a/python-closure/cum_average.py b/python-closure/cum_average.py new file mode 100644 index 0000000000..c1e960fb40 --- /dev/null +++ b/python-closure/cum_average.py @@ -0,0 +1,8 @@ +def cumulative_average(): + data = [] + + def average(value): + data.append(value) + return sum(data) / len(data) + + return average diff --git a/python-closure/decorators.py b/python-closure/decorators.py new file mode 100644 index 0000000000..93ba36b0dc --- /dev/null +++ b/python-closure/decorators.py @@ -0,0 +1,12 @@ +def decorator(function): + def closure(): + print("Doing something before calling the function.") + function() + print("Doing something after calling the function.") + + return closure + + +@decorator +def greet(): + print("Hi, Pythonista!") diff --git a/python-closure/free_variables.py b/python-closure/free_variables.py new file mode 100644 index 0000000000..a24753e361 --- /dev/null +++ b/python-closure/free_variables.py @@ -0,0 +1,11 @@ +def outer_func(outer_arg): + local_var = "Outer local variable" + + def closure(): + print(outer_arg) + print(local_var) + print(another_local_var) + + another_local_var = "Another outer local variable" + + return closure diff --git a/python-closure/inner.py b/python-closure/inner.py new file mode 100644 index 0000000000..4811053b77 --- /dev/null +++ b/python-closure/inner.py @@ -0,0 +1,7 @@ +def outer_func(): + name = "Pythonista" + + def inner_func(): + print(f"Hello, {name}!") + + inner_func() diff --git a/python-closure/memoization.py b/python-closure/memoization.py new file mode 100644 index 0000000000..cd98829679 --- /dev/null +++ b/python-closure/memoization.py @@ -0,0 +1,27 @@ +from time import sleep +from timeit import timeit + + +def memoize(function): + cache = {} + + def closure(number): + if number not in cache: + cache[number] = function(number) + return cache[number] + + return closure + + +@memoize +def slow_operation(number): + sleep(0.5) + + +exec_time = timeit( + "[slow_operation(number) for number in [2, 3, 4, 2, 3, 4]]", + globals=globals(), + number=1, +) + +print(f"Slow operation's time: {exec_time:.2f} seconds.") diff --git a/python-closure/roots.py b/python-closure/roots.py new file mode 100644 index 0000000000..57275bc3da --- /dev/null +++ b/python-closure/roots.py @@ -0,0 +1,28 @@ +def make_root_calculator(root_degree, precision=2): + def root_calculator(number): + return round(pow(number, 1 / root_degree), precision) + + return root_calculator + + +square_root = make_root_calculator(2, 4) +print(square_root(42)) + +cubic_root = make_root_calculator(3, 2) +print(cubic_root(42)) + + +class RootCalculator: + def __init__(self, root_degree, precision=2): + self.root_degree = root_degree + self.precision = precision + + def __call__(self, number): + return round(pow(number, 1 / self.root_degree), self.precision) + + +square_root = RootCalculator(2, 4) +print(square_root(42)) + +cubic_root = RootCalculator(3, 2) +print(cubic_root(42)) diff --git a/python-closure/stack_class.py b/python-closure/stack_class.py new file mode 100644 index 0000000000..51fa654889 --- /dev/null +++ b/python-closure/stack_class.py @@ -0,0 +1,12 @@ +class Stack: + def __init__(self): + self._items = [] + + def push(self, item): + self._items.append(item) + + def pop(self): + return self._items.pop() + + def __str__(self): + return str(self._items) diff --git a/python-closure/stack_closure.py b/python-closure/stack_closure.py new file mode 100644 index 0000000000..36c7bc664e --- /dev/null +++ b/python-closure/stack_closure.py @@ -0,0 +1,15 @@ +def Stack(): + _items = [] + + def push(item): + _items.append(item) + + def pop(): + return _items.pop() + + def closure(): + pass + + closure.push = push + closure.pop = pop + return closure From d60d0a946968836a219aceeff134a77363ff23de Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Mon, 16 Sep 2024 17:23:58 +0200 Subject: [PATCH 11/29] TR updates, first round --- syntactic-sugar-python/README.md | 4 ++-- syntactic-sugar-python/timing.py | 2 ++ syntactic-sugar-python/twice.py | 12 ++++++++++++ syntactic-sugar-python/user_input.py | 7 +++++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 syntactic-sugar-python/twice.py diff --git a/syntactic-sugar-python/README.md b/syntactic-sugar-python/README.md index 71228b1e5f..3808952bbd 100644 --- a/syntactic-sugar-python/README.md +++ b/syntactic-sugar-python/README.md @@ -1,3 +1,3 @@ -# Syntactic Sugar in Python: Make Your Code Pythonic +# Syntactic Sugar in Python: Making Your Code Pythonic -This folder provides the code examples for the Real Python tutorial [Syntactic Sugar in Python: Make Your Code Pythonic](https://realpython.com/python-syntactic-sugar/). +This folder provides the code examples for the Real Python tutorial [Syntactic Sugar in Python: Making Your Code Pythonic](https://realpython.com/python-syntactic-sugar/). diff --git a/syntactic-sugar-python/timing.py b/syntactic-sugar-python/timing.py index 52da01c230..0be2f7d75a 100644 --- a/syntactic-sugar-python/timing.py +++ b/syntactic-sugar-python/timing.py @@ -1,7 +1,9 @@ +import functools import time def timer(func): + @functools.wraps(func) def _timer(*args, **kwargs): start = time.time() result = func(*args, **kwargs) diff --git a/syntactic-sugar-python/twice.py b/syntactic-sugar-python/twice.py new file mode 100644 index 0000000000..463a27a568 --- /dev/null +++ b/syntactic-sugar-python/twice.py @@ -0,0 +1,12 @@ +class Twice: + def __init__(self, items): + self.items = list(items) + + def __iter__(self): + yield from self.items + print("Halfway there!") + yield from self.items + + +for number in Twice([1, 2, 3]): + print(f"-> {number}") diff --git a/syntactic-sugar-python/user_input.py b/syntactic-sugar-python/user_input.py index 1b0acd68ef..da135869be 100644 --- a/syntactic-sugar-python/user_input.py +++ b/syntactic-sugar-python/user_input.py @@ -3,3 +3,10 @@ while line != "stop": print(line) line = input("Type some text: ") + + +while True: + line = input("Type some text: ") + if line == "stop": + break + print(line) From c5edc5c31e435d7e167b522b007e3ced6b2f5a32 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 17 Sep 2024 12:47:19 +0200 Subject: [PATCH 12/29] TR updates, second round --- syntactic-sugar-python/README.md | 4 ++-- syntactic-sugar-python/circle.py | 4 ++-- syntactic-sugar-python/timing.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/syntactic-sugar-python/README.md b/syntactic-sugar-python/README.md index 3808952bbd..9890415043 100644 --- a/syntactic-sugar-python/README.md +++ b/syntactic-sugar-python/README.md @@ -1,3 +1,3 @@ -# Syntactic Sugar in Python: Making Your Code Pythonic +# Syntactic Sugar: Why Python is Sweet and Pythonic -This folder provides the code examples for the Real Python tutorial [Syntactic Sugar in Python: Making Your Code Pythonic](https://realpython.com/python-syntactic-sugar/). +This folder provides the code examples for the Real Python tutorial [Syntactic Sugar: Why Python is Sweet and Pythonic](https://realpython.com/python-syntactic-sugar/). diff --git a/syntactic-sugar-python/circle.py b/syntactic-sugar-python/circle.py index 90baeceace..8cb9e94273 100644 --- a/syntactic-sugar-python/circle.py +++ b/syntactic-sugar-python/circle.py @@ -8,11 +8,11 @@ def __init__(self, radius): def area(self): return pi * self.radius**2 - def perimeter(self): + def circumference(self): return 2 * pi * self.radius circle = Circle(10) print(circle.radius) print(circle.area()) -print(circle.perimeter()) +print(circle.circumference()) diff --git a/syntactic-sugar-python/timing.py b/syntactic-sugar-python/timing.py index 0be2f7d75a..484ea8a4c3 100644 --- a/syntactic-sugar-python/timing.py +++ b/syntactic-sugar-python/timing.py @@ -5,9 +5,9 @@ def timer(func): @functools.wraps(func) def _timer(*args, **kwargs): - start = time.time() + start = time.perf_counter() result = func(*args, **kwargs) - end = time.time() + end = time.perf_counter() print(f"Execution time: {end - start:.4f} seconds") return result From 61d47865347c77f814532674b08c2ba8c2b3f98c Mon Sep 17 00:00:00 2001 From: gahjelle Date: Wed, 18 Sep 2024 08:44:04 +0200 Subject: [PATCH 13/29] Add more output --- syntactic-sugar-python/balance.py | 13 +++++++++++-- syntactic-sugar-python/membership.py | 6 ++++++ syntactic-sugar-python/stack.py | 11 +++++++++++ syntactic-sugar-python/timing.py | 2 +- syntactic-sugar-python/user_input.py | 7 +++++++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/syntactic-sugar-python/balance.py b/syntactic-sugar-python/balance.py index 3e84d19133..83fcaf468d 100644 --- a/syntactic-sugar-python/balance.py +++ b/syntactic-sugar-python/balance.py @@ -1,5 +1,5 @@ -debit = 300.00 -credit = 450.00 +debit = 300 +credit = 450 print( f"Debit: ${debit:.2f}, Credit: ${credit:.2f}, Balance: ${credit - debit:.2f}" @@ -10,3 +10,12 @@ debit, credit, credit - debit ) ) + +print( + "Debit: $" + + format(debit, ".2f") + + ", Credit: $" + + format(credit, ".2f") + + ", Balance: $" + + format(credit - debit, ".2f") +) diff --git a/syntactic-sugar-python/membership.py b/syntactic-sugar-python/membership.py index 49af2d913f..0178e01c83 100644 --- a/syntactic-sugar-python/membership.py +++ b/syntactic-sugar-python/membership.py @@ -3,3 +3,9 @@ def is_member(value, iterable): if current_value == value: return True return False + + +print(is_member(5, [1, 2, 3, 4, 5])) +print(not is_member(5, [1, 2, 3, 4, 5])) +print(is_member(100, [1, 2, 3, 4, 5])) +print(not is_member(100, [1, 2, 3, 4, 5])) diff --git a/syntactic-sugar-python/stack.py b/syntactic-sugar-python/stack.py index a551895da3..edb8180808 100644 --- a/syntactic-sugar-python/stack.py +++ b/syntactic-sugar-python/stack.py @@ -17,3 +17,14 @@ def __iter__(self): # def __iter__(self): # for item in self.items: # yield item + + +stack = Stack() +stack.push("one") +stack.push("two") +stack.push("three") + +for item in stack: + print(item) + +print(stack.pop()) diff --git a/syntactic-sugar-python/timing.py b/syntactic-sugar-python/timing.py index 484ea8a4c3..ed2ed08678 100644 --- a/syntactic-sugar-python/timing.py +++ b/syntactic-sugar-python/timing.py @@ -20,4 +20,4 @@ def delayed_mean(sample): return sum(sample) / len(sample) -delayed_mean([10, 2, 4, 7, 9, 3, 9, 8, 6, 7]) +print(delayed_mean([10, 2, 4, 7, 9, 3, 9, 8, 6, 7])) diff --git a/syntactic-sugar-python/user_input.py b/syntactic-sugar-python/user_input.py index da135869be..8b997ed357 100644 --- a/syntactic-sugar-python/user_input.py +++ b/syntactic-sugar-python/user_input.py @@ -1,3 +1,9 @@ +# With assignment operator +while (line := input("Type some text: ")) != "stop": + print(line) + + +# With assignment before loop line = input("Type some text: ") while line != "stop": @@ -5,6 +11,7 @@ line = input("Type some text: ") +# With condition inside loop while True: line = input("Type some text: ") if line == "stop": From 9990855f21fb221176dce6fdf1071bfea6818b97 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Thu, 19 Sep 2024 11:03:03 +0200 Subject: [PATCH 14/29] Sample code for the article on properties --- python-property/README.md | 3 +++ python-property/circle_v1.py | 22 ++++++++++++++++++++++ python-property/circle_v2.py | 19 +++++++++++++++++++ python-property/circle_v3.py | 19 +++++++++++++++++++ python-property/circle_v4.py | 13 +++++++++++++ python-property/circle_v5.py | 21 +++++++++++++++++++++ python-property/circle_v6.py | 11 +++++++++++ python-property/circle_v7.py | 12 ++++++++++++ python-property/circle_v8.py | 25 +++++++++++++++++++++++++ python-property/currency_v1.py | 6 ++++++ python-property/currency_v2.py | 23 +++++++++++++++++++++++ python-property/node.py | 23 +++++++++++++++++++++++ python-property/persons.py | 20 ++++++++++++++++++++ python-property/point_v1.py | 16 ++++++++++++++++ python-property/point_v2.py | 12 ++++++++++++ python-property/point_v3.py | 23 +++++++++++++++++++++++ python-property/point_v4.py | 28 ++++++++++++++++++++++++++++ python-property/point_v5.py | 21 +++++++++++++++++++++ python-property/point_v6.py | 21 +++++++++++++++++++++ python-property/product.py | 8 ++++++++ python-property/rectangle.py | 8 ++++++++ python-property/users.py | 18 ++++++++++++++++++ 22 files changed, 372 insertions(+) create mode 100644 python-property/README.md create mode 100644 python-property/circle_v1.py create mode 100644 python-property/circle_v2.py create mode 100644 python-property/circle_v3.py create mode 100644 python-property/circle_v4.py create mode 100644 python-property/circle_v5.py create mode 100644 python-property/circle_v6.py create mode 100644 python-property/circle_v7.py create mode 100644 python-property/circle_v8.py create mode 100644 python-property/currency_v1.py create mode 100644 python-property/currency_v2.py create mode 100644 python-property/node.py create mode 100644 python-property/persons.py create mode 100644 python-property/point_v1.py create mode 100644 python-property/point_v2.py create mode 100644 python-property/point_v3.py create mode 100644 python-property/point_v4.py create mode 100644 python-property/point_v5.py create mode 100644 python-property/point_v6.py create mode 100644 python-property/product.py create mode 100644 python-property/rectangle.py create mode 100644 python-property/users.py diff --git a/python-property/README.md b/python-property/README.md new file mode 100644 index 0000000000..0697106df6 --- /dev/null +++ b/python-property/README.md @@ -0,0 +1,3 @@ +# Python's property(): Add Managed Attributes to Your Classes + +This folder provides the code examples for the Real Python tutorial [Python's property(): Add Managed Attributes to Your Classes](https://realpython.com/python-property/). diff --git a/python-property/circle_v1.py b/python-property/circle_v1.py new file mode 100644 index 0000000000..f536e9042b --- /dev/null +++ b/python-property/circle_v1.py @@ -0,0 +1,22 @@ +class Circle: + def __init__(self, radius): + self._radius = radius + + def _get_radius(self): + print("Get radius") + return self._radius + + def _set_radius(self, value): + print("Set radius") + self._radius = value + + def _del_radius(self): + print("Delete radius") + del self._radius + + radius = property( + fget=_get_radius, + fset=_set_radius, + fdel=_del_radius, + doc="The radius property." + ) diff --git a/python-property/circle_v2.py b/python-property/circle_v2.py new file mode 100644 index 0000000000..b08bae770a --- /dev/null +++ b/python-property/circle_v2.py @@ -0,0 +1,19 @@ +class Circle: + def __init__(self, radius): + self._radius = radius + + @property + def radius(self): + """The radius property.""" + print("Get radius") + return self._radius + + @radius.setter + def radius(self, value): + print("Set radius") + self._radius = value + + @radius.deleter + def radius(self): + print("Delete radius") + del self._radius diff --git a/python-property/circle_v3.py b/python-property/circle_v3.py new file mode 100644 index 0000000000..894df96f99 --- /dev/null +++ b/python-property/circle_v3.py @@ -0,0 +1,19 @@ +class Circle: + def __init__(self, radius): + self.radius = radius + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._radius = float(value) + + @property + def diameter(self): + return self.radius * 2 + + @diameter.setter + def diameter(self, value): + self.radius = value / 2 diff --git a/python-property/circle_v4.py b/python-property/circle_v4.py new file mode 100644 index 0000000000..3085f35f8a --- /dev/null +++ b/python-property/circle_v4.py @@ -0,0 +1,13 @@ +from time import sleep + +class Circle: + def __init__(self, radius): + self.radius = radius + self._diameter = None + + @property + def diameter(self): + if self._diameter is None: + sleep(0.5) # Simulate a costly computation + self._diameter = self.radius * 2 + return self._diameter diff --git a/python-property/circle_v5.py b/python-property/circle_v5.py new file mode 100644 index 0000000000..7803a086bf --- /dev/null +++ b/python-property/circle_v5.py @@ -0,0 +1,21 @@ +from time import sleep + +class Circle: + def __init__(self, radius): + self.radius = radius + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._diameter = None + self._radius = value + + @property + def diameter(self): + if self._diameter is None: + sleep(0.5) # Simulate a costly computation + self._diameter = self._radius * 2 + return self._diameter diff --git a/python-property/circle_v6.py b/python-property/circle_v6.py new file mode 100644 index 0000000000..6c20d6ac5f --- /dev/null +++ b/python-property/circle_v6.py @@ -0,0 +1,11 @@ +from functools import cached_property +from time import sleep + +class Circle: + def __init__(self, radius): + self.radius = radius + + @cached_property + def diameter(self): + sleep(0.5) # Simulate a costly computation + return self.radius * 2 diff --git a/python-property/circle_v7.py b/python-property/circle_v7.py new file mode 100644 index 0000000000..2b5f226f40 --- /dev/null +++ b/python-property/circle_v7.py @@ -0,0 +1,12 @@ +from functools import cache +from time import sleep + +class Circle: + def __init__(self, radius): + self.radius = radius + + @property + @cache + def diameter(self): + sleep(0.5) # Simulate a costly computation + return self.radius * 2 diff --git a/python-property/circle_v8.py b/python-property/circle_v8.py new file mode 100644 index 0000000000..684015ab4b --- /dev/null +++ b/python-property/circle_v8.py @@ -0,0 +1,25 @@ +import logging + +logging.basicConfig( + format="%(asctime)s: %(message)s", + level=logging.INFO, + datefmt="%H:%M:%S" +) + +class Circle: + def __init__(self, radius): + self._msg = '"radius" was %s. Current value: %s' + self.radius = radius + + @property + def radius(self): + logging.info(self._msg % ("accessed", str(self._radius))) + return self._radius + + @radius.setter + def radius(self, value): + try: + self._radius = float(value) + logging.info(self._msg % ("mutated", str(self._radius))) + except ValueError: + logging.info('validation error while mutating "radius"') diff --git a/python-property/currency_v1.py b/python-property/currency_v1.py new file mode 100644 index 0000000000..427a63fee0 --- /dev/null +++ b/python-property/currency_v1.py @@ -0,0 +1,6 @@ +class Currency: + def __init__(self, units, cents): + self.units = units + self.cents = cents + + # Currency implementation... diff --git a/python-property/currency_v2.py b/python-property/currency_v2.py new file mode 100644 index 0000000000..c252ab1992 --- /dev/null +++ b/python-property/currency_v2.py @@ -0,0 +1,23 @@ +CENTS_PER_UNIT = 100 + +class Currency: + def __init__(self, units, cents): + self._total_cents = units * CENTS_PER_UNIT + cents + + @property + def units(self): + return self._total_cents // CENTS_PER_UNIT + + @units.setter + def units(self, value): + self._total_cents = self.cents + value * CENTS_PER_UNIT + + @property + def cents(self): + return self._total_cents % CENTS_PER_UNIT + + @cents.setter + def cents(self, value): + self._total_cents = self.units * CENTS_PER_UNIT + value + + # Currency implementation... diff --git a/python-property/node.py b/python-property/node.py new file mode 100644 index 0000000000..071240a274 --- /dev/null +++ b/python-property/node.py @@ -0,0 +1,23 @@ +class TreeNode: + def __init__(self, data): + self._data = data + self._children = [] + + @property + def children(self): + return self._children + + @children.setter + def children(self, value): + if isinstance(value, list): + self._children = value + else: + del self.children + self._children.append(value) + + @children.deleter + def children(self): + self._children.clear() + + def __repr__(self): + return f'{self.__class__.__name__}("{self._data}")' diff --git a/python-property/persons.py b/python-property/persons.py new file mode 100644 index 0000000000..4bb5faff4b --- /dev/null +++ b/python-property/persons.py @@ -0,0 +1,20 @@ +class Person: + def __init__(self, name): + self._name = name + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + # Person implementation... + +class Employee(Person): + @property + def name(self): + return super().name.upper() + + # Employee implementation... diff --git a/python-property/point_v1.py b/python-property/point_v1.py new file mode 100644 index 0000000000..216fc89dec --- /dev/null +++ b/python-property/point_v1.py @@ -0,0 +1,16 @@ +class Point: + def __init__(self, x, y): + self._x = x + self._y = y + + def get_x(self): + return self._x + + def set_x(self, value): + self._x = value + + def get_y(self): + return self._y + + def set_y(self, value): + self._y = value diff --git a/python-property/point_v2.py b/python-property/point_v2.py new file mode 100644 index 0000000000..9222dac9f2 --- /dev/null +++ b/python-property/point_v2.py @@ -0,0 +1,12 @@ +class Point: + def __init__(self, x, y): + self._x = x + self._y = y + + @property + def x(self): + return self._x + + @property + def y(self): + return self._y diff --git a/python-property/point_v3.py b/python-property/point_v3.py new file mode 100644 index 0000000000..946a044155 --- /dev/null +++ b/python-property/point_v3.py @@ -0,0 +1,23 @@ +class WriteCoordinateError(Exception): + pass + +class Point: + def __init__(self, x, y): + self._x = x + self._y = y + + @property + def x(self): + return self._x + + @x.setter + def x(self, value): + raise WriteCoordinateError("x coordinate is read-only") + + @property + def y(self): + return self._y + + @y.setter + def y(self, value): + raise WriteCoordinateError("y coordinate is read-only") diff --git a/python-property/point_v4.py b/python-property/point_v4.py new file mode 100644 index 0000000000..0009fd18f0 --- /dev/null +++ b/python-property/point_v4.py @@ -0,0 +1,28 @@ +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + @property + def x(self): + return self._x + + @x.setter + def x(self, value): + try: + self._x = float(value) + print("Validated!") + except ValueError: + raise ValueError('"x" must be a number') from None + + @property + def y(self): + return self._y + + @y.setter + def y(self, value): + try: + self._y = float(value) + print("Validated!") + except ValueError: + raise ValueError('"y" must be a number') from None diff --git a/python-property/point_v5.py b/python-property/point_v5.py new file mode 100644 index 0000000000..cd6d5fa393 --- /dev/null +++ b/python-property/point_v5.py @@ -0,0 +1,21 @@ +class Coordinate: + def __set_name__(self, owner, name): + self._name = name + + def __get__(self, instance, owner): + return instance.__dict__[self._name] + + def __set__(self, instance, value): + try: + instance.__dict__[self._name] = float(value) + print("Validated!") + except ValueError: + raise ValueError(f'"{self._name}" must be a number') from None + +class Point: + x = Coordinate() + y = Coordinate() + + def __init__(self, x, y): + self.x = x + self.y = y diff --git a/python-property/point_v6.py b/python-property/point_v6.py new file mode 100644 index 0000000000..c974021d6d --- /dev/null +++ b/python-property/point_v6.py @@ -0,0 +1,21 @@ +import math + + +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + @property + def distance(self): + return round(math.dist((0, 0), (self.x, self.y))) + + @property + def angle(self): + return round(math.degrees(math.atan(self.y / self.x)), 1) + + def as_cartesian(self): + return self.x, self.y + + def as_polar(self): + return self.distance, self.angle diff --git a/python-property/product.py b/python-property/product.py new file mode 100644 index 0000000000..f40ebf903b --- /dev/null +++ b/python-property/product.py @@ -0,0 +1,8 @@ +class Product: + def __init__(self, name, price): + self._name = name + self._price = float(price) + + @property + def price(self): + return f"${self._price:,.2f}" diff --git a/python-property/rectangle.py b/python-property/rectangle.py new file mode 100644 index 0000000000..f3800f0005 --- /dev/null +++ b/python-property/rectangle.py @@ -0,0 +1,8 @@ +class Rectangle: + def __init__(self, width, height): + self.width = width + self.height = height + + @property + def area(self): + return self.width * self.height diff --git a/python-property/users.py b/python-property/users.py new file mode 100644 index 0000000000..d0ed37d7fb --- /dev/null +++ b/python-property/users.py @@ -0,0 +1,18 @@ +import hashlib +import os + +class User: + def __init__(self, name, password): + self.name = name + self.password = password + + @property + def password(self): + raise AttributeError("Password is write-only") + + @password.setter + def password(self, plaintext): + salt = os.urandom(32) + self._hashed_password = hashlib.pbkdf2_hmac( + "sha256", plaintext.encode("utf-8"), salt, 100_000 + ) From efaedc12a58c857aa674210d39bb03ae7cace224 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Thu, 19 Sep 2024 11:08:25 +0200 Subject: [PATCH 15/29] Fix linter issues --- python-property/circle_v4.py | 1 + python-property/circle_v5.py | 1 + python-property/circle_v6.py | 1 + python-property/circle_v7.py | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/python-property/circle_v4.py b/python-property/circle_v4.py index 3085f35f8a..45d6637357 100644 --- a/python-property/circle_v4.py +++ b/python-property/circle_v4.py @@ -1,5 +1,6 @@ from time import sleep + class Circle: def __init__(self, radius): self.radius = radius diff --git a/python-property/circle_v5.py b/python-property/circle_v5.py index 7803a086bf..2c2e592b4c 100644 --- a/python-property/circle_v5.py +++ b/python-property/circle_v5.py @@ -1,5 +1,6 @@ from time import sleep + class Circle: def __init__(self, radius): self.radius = radius diff --git a/python-property/circle_v6.py b/python-property/circle_v6.py index 6c20d6ac5f..acf8ef7b35 100644 --- a/python-property/circle_v6.py +++ b/python-property/circle_v6.py @@ -1,6 +1,7 @@ from functools import cached_property from time import sleep + class Circle: def __init__(self, radius): self.radius = radius diff --git a/python-property/circle_v7.py b/python-property/circle_v7.py index 2b5f226f40..18a571625e 100644 --- a/python-property/circle_v7.py +++ b/python-property/circle_v7.py @@ -1,6 +1,7 @@ from functools import cache from time import sleep + class Circle: def __init__(self, radius): self.radius = radius @@ -8,5 +9,5 @@ def __init__(self, radius): @property @cache def diameter(self): - sleep(0.5) # Simulate a costly computation + sleep(0.5) # Simulate a costly computation return self.radius * 2 From 42658d6cd64663d0ae4877082a40d1811e103910 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Thu, 19 Sep 2024 11:11:21 +0200 Subject: [PATCH 16/29] More linter fixes --- python-property/users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python-property/users.py b/python-property/users.py index d0ed37d7fb..137103c93b 100644 --- a/python-property/users.py +++ b/python-property/users.py @@ -1,6 +1,7 @@ import hashlib import os + class User: def __init__(self, name, password): self.name = name From c20c62a95918b2d83bf0437b1b20bd1dc94c3230 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Thu, 19 Sep 2024 11:27:12 +0200 Subject: [PATCH 17/29] More fixes --- python-property/circle_v1.py | 2 +- python-property/circle_v8.py | 3 ++- python-property/currency_v2.py | 1 + python-property/persons.py | 1 + python-property/point_v3.py | 1 + python-property/point_v5.py | 1 + 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/python-property/circle_v1.py b/python-property/circle_v1.py index f536e9042b..3103dded65 100644 --- a/python-property/circle_v1.py +++ b/python-property/circle_v1.py @@ -18,5 +18,5 @@ def _del_radius(self): fget=_get_radius, fset=_set_radius, fdel=_del_radius, - doc="The radius property." + doc="The radius property.", ) diff --git a/python-property/circle_v8.py b/python-property/circle_v8.py index 684015ab4b..f3ba423ce0 100644 --- a/python-property/circle_v8.py +++ b/python-property/circle_v8.py @@ -3,9 +3,10 @@ logging.basicConfig( format="%(asctime)s: %(message)s", level=logging.INFO, - datefmt="%H:%M:%S" + datefmt="%H:%M:%S", ) + class Circle: def __init__(self, radius): self._msg = '"radius" was %s. Current value: %s' diff --git a/python-property/currency_v2.py b/python-property/currency_v2.py index c252ab1992..159dcdc131 100644 --- a/python-property/currency_v2.py +++ b/python-property/currency_v2.py @@ -1,5 +1,6 @@ CENTS_PER_UNIT = 100 + class Currency: def __init__(self, units, cents): self._total_cents = units * CENTS_PER_UNIT + cents diff --git a/python-property/persons.py b/python-property/persons.py index 4bb5faff4b..af70842914 100644 --- a/python-property/persons.py +++ b/python-property/persons.py @@ -12,6 +12,7 @@ def name(self, value): # Person implementation... + class Employee(Person): @property def name(self): diff --git a/python-property/point_v3.py b/python-property/point_v3.py index 946a044155..f3d05daa29 100644 --- a/python-property/point_v3.py +++ b/python-property/point_v3.py @@ -1,6 +1,7 @@ class WriteCoordinateError(Exception): pass + class Point: def __init__(self, x, y): self._x = x diff --git a/python-property/point_v5.py b/python-property/point_v5.py index cd6d5fa393..64650b5645 100644 --- a/python-property/point_v5.py +++ b/python-property/point_v5.py @@ -12,6 +12,7 @@ def __set__(self, instance, value): except ValueError: raise ValueError(f'"{self._name}" must be a number') from None + class Point: x = Coordinate() y = Coordinate() From e52d11bbe4aaea9f9a8bb9afbd760eaabdb73dcc Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Fri, 20 Sep 2024 22:48:48 +0200 Subject: [PATCH 18/29] Final QA (#584) --- contact-book-python-textual/source_code/rpcontacts/tui.py | 2 +- .../source_code_step_6/rpcontacts/tui.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contact-book-python-textual/source_code/rpcontacts/tui.py b/contact-book-python-textual/source_code/rpcontacts/tui.py index fd46db97ad..92a5a5a62f 100644 --- a/contact-book-python-textual/source_code/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code/rpcontacts/tui.py @@ -102,7 +102,7 @@ def check_answer(accepted): self.query_one(DataTable).clear() self.push_screen( - QuestionDialog("Are you sure you want te remove all contacts?"), + QuestionDialog("Are you sure you want to remove all contacts?"), check_answer, ) diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py index fd46db97ad..92a5a5a62f 100644 --- a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py @@ -102,7 +102,7 @@ def check_answer(accepted): self.query_one(DataTable).clear() self.push_screen( - QuestionDialog("Are you sure you want te remove all contacts?"), + QuestionDialog("Are you sure you want to remove all contacts?"), check_answer, ) From b9a0481371e52109229391e0e3a8d1c67a277783 Mon Sep 17 00:00:00 2001 From: gahjelle Date: Tue, 24 Sep 2024 10:55:03 +0200 Subject: [PATCH 19/29] Add materials for Python 3.13 tutorials --- python-313/README.md | 31 +++ python-313/free-threading-jit/Dockerfile | 48 ++++ python-313/free-threading-jit/README.md | 218 ++++++++++++++++++ .../free-threading-jit/benchmarks/gil.py | 72 ++++++ .../free-threading-jit/benchmarks/jit.py | 76 ++++++ .../benchmarks/pyfeatures.py | 64 +++++ .../free-threading-jit/benchmarks/pyinfo.py | 49 ++++ .../free-threading-jit/benchmarks/uops.py | 30 +++ .../free-threading-jit/c_extension/greeter.c | 28 +++ .../c_extension/greeter_threaded.c | 39 ++++ .../csv_data/gil_i5-1035G7.csv | 206 +++++++++++++++++ .../csv_data/gil_i5-4570S.csv | 201 ++++++++++++++++ .../csv_data/gil_i7-6600U.csv | 209 +++++++++++++++++ .../csv_data/jit_i5-4570S.csv | 136 +++++++++++ python-313/repl/guessing_game.py | 18 ++ python-313/repl/multiline_editing.py | 10 + python-313/repl/power_factory.py | 12 + python-313/repl/tab_completion.py | 8 + 18 files changed, 1455 insertions(+) create mode 100644 python-313/README.md create mode 100644 python-313/free-threading-jit/Dockerfile create mode 100644 python-313/free-threading-jit/README.md create mode 100644 python-313/free-threading-jit/benchmarks/gil.py create mode 100644 python-313/free-threading-jit/benchmarks/jit.py create mode 100644 python-313/free-threading-jit/benchmarks/pyfeatures.py create mode 100644 python-313/free-threading-jit/benchmarks/pyinfo.py create mode 100644 python-313/free-threading-jit/benchmarks/uops.py create mode 100644 python-313/free-threading-jit/c_extension/greeter.c create mode 100644 python-313/free-threading-jit/c_extension/greeter_threaded.c create mode 100644 python-313/free-threading-jit/csv_data/gil_i5-1035G7.csv create mode 100644 python-313/free-threading-jit/csv_data/gil_i5-4570S.csv create mode 100644 python-313/free-threading-jit/csv_data/gil_i7-6600U.csv create mode 100644 python-313/free-threading-jit/csv_data/jit_i5-4570S.csv create mode 100644 python-313/repl/guessing_game.py create mode 100644 python-313/repl/multiline_editing.py create mode 100644 python-313/repl/power_factory.py create mode 100644 python-313/repl/tab_completion.py diff --git a/python-313/README.md b/python-313/README.md new file mode 100644 index 0000000000..f9f092032c --- /dev/null +++ b/python-313/README.md @@ -0,0 +1,31 @@ +# Python 3.13 Demos + +This repository contains sample code and data files that demos some of the new features in Python 3.13. + +## Introduction + +You need Python 3.13 installed to run these examples. See the following tutorial instructions: + +- [How Can You Install a Pre-Release Version of Python](https://realpython.com/python-pre-release/) + +Note that for testing the free-threading and JIT features, you'll need to build Python from source code with additional compiler flags enabled, as [explained in the tutorial](https://realpython.com/python313-free-threading-jit/#get-your-hands-on-the-new-features). Alternatively, you can run benchmarks using Docker containers as [explained below](#free-threading-and-jit). + +You can learn more about Python 3.13's new features in the following Real Python tutorials: + + +- [Python 3.13 Preview: Free Threading and a JIT Compiler](https://realpython.com/python313-free-threading-jit/) +- [Python 3.13 Preview: A Modern REPL](https://realpython.com/python313-repl) + +You'll find examples from these tutorials in this repository. + +## Examples + +This section only contains brief instructions on how you can run the examples. See the tutorials for technical details. + +### REPL + + + +### Free-Threading and JIT + +You need to enable a few build options to try out the free-threading and JIT features in Python 3.13. You can find more information in the dedicated [README file](free-threading-jit/README.md). \ No newline at end of file diff --git a/python-313/free-threading-jit/Dockerfile b/python-313/free-threading-jit/Dockerfile new file mode 100644 index 0000000000..19852876ca --- /dev/null +++ b/python-313/free-threading-jit/Dockerfile @@ -0,0 +1,48 @@ +FROM ubuntu:latest AS free-threaded-builder + +# Install dependencies +RUN apt update && \ + apt upgrade -y && \ + DEBIAN_FRONTEND=noninteractive apt install -y \ + wget unzip build-essential pkg-config zlib1g-dev python3 clang + +# Download Python 3.13 source code +ARG BASE_URL=https://github.com/python/cpython/archive/refs/tags +ARG ZIP_FILE=v3.13.0rc1.zip +RUN wget -P /tmp $BASE_URL/$ZIP_FILE && \ + unzip -d /tmp /tmp/$ZIP_FILE + +# Build free-threaded Python 3.13 +WORKDIR /tmp/cpython-3.13.0rc1/ +RUN ./configure --disable-gil --enable-experimental-jit --enable-optimizations && \ + make -j$(nproc) && \ + make altinstall + +FROM free-threaded-builder AS jit-builder + +# Build Python 3.13 with JIT only +WORKDIR /tmp/cpython-3.13.0rc1/ +RUN make clean && \ + ./configure --enable-experimental-jit --enable-optimizations --prefix=/python3.13-jit && \ + make -j$(nproc) && \ + make install && \ + ln -s /python3.13-jit/bin/python3.13 /usr/local/bin/python3.13j + +FROM jit-builder AS stock-builder + +# Build stock Python 3.13 +WORKDIR /tmp/cpython-3.13.0rc1/ +RUN make clean && \ + ./configure --enable-optimizations && \ + make -j$(nproc) && \ + make install + +FROM ubuntu:latest AS final + +COPY --from=jit-builder /python3.13-jit /python3.13-jit +COPY --from=stock-builder /usr/local /usr/local + +# Install Python provided by the system +RUN apt update && \ + apt install -y python3 && \ + apt clean && rm -rf /var/lib/apt/lists/* diff --git a/python-313/free-threading-jit/README.md b/python-313/free-threading-jit/README.md new file mode 100644 index 0000000000..693c1df24b --- /dev/null +++ b/python-313/free-threading-jit/README.md @@ -0,0 +1,218 @@ +# Python 3.13 Preview: Free Threading and a JIT Compiler + +This repository contains sample code and data files for the [Python 3.13 Preview: Free Threading and a JIT Compiler](https://realpython.com/python313-free-threading-jit/) tutorial on Real Python. + +## Build the Docker Image + +Build Python 3.13 from source code using Docker: + +```sh +$ docker image build --tag pythons . +``` + +**Note:** If you want to compile your C extension modules, then you'll need to keep the intermediate stage with the required build tools: + +```sh +$ docker build --target stock-builder --tag builder . +$ docker image build --tag pythons . +``` + +## Run Performance Benchmarks Using Docker + +Change directory to the `benchmarks/` folder: + +```sh +$ cd benchmarks/ +``` + +### Free Threading + +Run the benchmark against Python 3.12 shipped with the system: + +```sh +$ docker run --rm -v "$(pwd)":/app -w /app pythons python3.12 gil.py --threads=4 +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.12.3 /usr/bin/python3.12 +Free Threading: unsupported +JIT Compiler: unsupported +====================================================== +Running 4 threads: 7.33s +``` + +Run the benchmark against stock Python 3.13: + +```sh +$ docker run --rm -v "$(pwd)":/app -w /app pythons python3.13 gil.py --threads=4 +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.13.0rc1 /usr/local/bin/python3.13 +Free Threading: unsupported +JIT Compiler: unsupported +====================================================== +Running 4 threads: 7.51s +``` + +Run the benchmark against the free-threaded Python 3.13 build with the GIL and JIT disabled: + +```sh +$ docker run --rm -v "$(pwd)":/app -w /app -e PYTHON_JIT=0 pythons python3.13t gil.py --threads=4 +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.13.0rc1 /usr/local/bin/python3.13t +Free Threading: enabled ✨ +JIT Compiler: disabled +====================================================== +Running 4 threads: 3.25s +``` + +Run the benchmark against the free-threaded Python 3.13 build with the GIL enabled and JIT disabled: + +```sh +$ docker run --rm -v "$(pwd)":/app -w /app -e PYTHON_JIT=0 pythons python3.13t -X gil=1 gil.py --threads=4 +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.13.0rc1 /usr/local/bin/python3.13t +Free Threading: disabled +JIT Compiler: disabled +====================================================== +Running 4 threads: 13.72s +``` + +### Just-In-Time (JIT) Compiler + +Run the benchmark against Python 3.12 shipped with the system: + +```sh +$ docker run --rm -v "$(pwd)":/app -w /app pythons python3.12 jit.py -n 10000 +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.12.3 /usr/bin/python3.12 +Free Threading: unsupported +JIT Compiler: unsupported +====================================================== +Running fib() 10,000 times: 6.06s +``` + +Run the benchmark against stock Python 3.13: + +```sh +$ docker run --rm -v "$(pwd)":/app -w /app pythons python3.13 jit.py -n 10000 +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.13.0rc1 /usr/local/bin/python3.13 +Free Threading: unsupported +JIT Compiler: unsupported +====================================================== +Running fib() 10,000 times: 5.68s +``` + +Run the benchmark against Python 3.13 with the JIT enabled but GIL unsupported: + +```sh +$ docker run --rm -v "$(pwd)":/app -w /app pythons python3.13j jit.py -n 10000 +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.13.0rc1 /usr/local/bin/python3.13j +Free Threading: unsupported +JIT Compiler: enabled ✨ +====================================================== +Running fib() 10,000 times: 5.27s +``` + +## Disassemble JIT's Micro-Ops + +Reveal mico-ops generated by the JIT compiler: + +```sh +$ cd benchmarks/ +$ docker run --rm -it -v "$(pwd)":/app -w /app pythons python3.13j -i uops.py +====================================================== +💻 Linux 64bit with 4x CPU cores (x86_64 Little Endian) +🐍 CPython 3.13.0rc1 /usr/local/bin/python3.13j +Free Threading: unsupported +JIT Compiler: enabled ✨ +====================================================== +>>> def fib(n): +... a, b = 0, 1 +... for _ in range(n): +... a, b = b, a + b +... return a +... + +>>> fib(10) +55 + +>>> reveal_code(fib) +Micro-ops unavailable + +>>> fib(10) +55 + +>>> reveal_code(fib) +_START_EXECUTOR +_TIER2_RESUME_CHECK +_ITER_CHECK_RANGE +_GUARD_NOT_EXHAUSTED_RANGE +_ITER_NEXT_RANGE +_STORE_FAST_3 +_LOAD_FAST_2 +_LOAD_FAST_1 +_LOAD_FAST_2 +_GUARD_BOTH_INT +_BINARY_OP_ADD_INT +_STORE_FAST_2 +_STORE_FAST_1 +_JUMP_TO_TOP +_DEOPT +_EXIT_TRACE +_EXIT_TRACE +_ERROR_POP_N +_EXIT_TRACE +_ERROR_POP_N +``` + +## Compile the C Extension Module + +Build an intermediate stage with the necessary build tools if you haven't before: + +```shell +$ docker build --target stock-builder --tag builder . +``` + +Change directory to the `c_extension/` folder: + +```sh +$ cd c_extension/ +``` + +Start a Docker container from the builder stage and set common compiler flags: + +```sh +$ docker run --rm -it -v "$(pwd)":/app -w /app builder +root@8312f61fbb6d:/app# CFLAGS='-shared -fPIC -O3' +``` + +Compile the `greeter` module for stock Python 3.13: + +```sh +root@8312f61fbb6d:/app# gcc greeter.c $CFLAGS \ + $(python3.13-config --includes) \ + -o greeter_stock.so +``` + +Compile the `greeter` module for the free-threaded Python 3.13: + +```sh +root@8312f61fbb6d:/app# gcc greeter.c $CFLAGS \ + $(python3.13t-config --includes) \ + -o greeter_threaded_v1.so +``` + +Compile the `greeter_threaded` module for the free-threaded Python 3.13: + +```sh +root@8312f61fbb6d:/app# gcc greeter_threaded.c $CFLAGS \ + $(python3.13t-config --includes) \ + -o greeter_threaded_v2.so +``` diff --git a/python-313/free-threading-jit/benchmarks/gil.py b/python-313/free-threading-jit/benchmarks/gil.py new file mode 100644 index 0000000000..331fd78046 --- /dev/null +++ b/python-313/free-threading-jit/benchmarks/gil.py @@ -0,0 +1,72 @@ +from argparse import ArgumentParser +from concurrent.futures import ThreadPoolExecutor +from csv import DictWriter +from functools import wraps +from os import cpu_count +from pathlib import Path +from time import perf_counter +from typing import NamedTuple + +from pyinfo import print_details, python_short + +CSV_PATH = Path(__file__).with_suffix(".csv") +DEFAULT_N = 35 + + +class Record(NamedTuple): + python: str + threads: int + seconds: float + + def save(self): + empty = not CSV_PATH.exists() + with CSV_PATH.open(mode="a", encoding="utf-8", newline="") as file: + writer = DictWriter(file, Record._fields) + if empty: + writer.writeheader() + writer.writerow(self._asdict()) + + +def parse_args(): + parser = ArgumentParser() + parser.add_argument("-t", "--threads", type=int, default=cpu_count()) + parser.add_argument("-n", type=int, default=DEFAULT_N) + return parser.parse_args() + + +def main(args): + print_details() + benchmark(args.threads, args.n) + + +def timed(function): + @wraps(function) + def wrapper(num_threads, n): + t1 = perf_counter() + result = function(num_threads, n) + t2 = perf_counter() + duration = t2 - t1 + print(f"\b\b\b: {duration:.2f}s") + Record(python_short(), num_threads, duration).save() + return result + + return wrapper + + +@timed +def benchmark(num_threads, n): + with ThreadPoolExecutor(max_workers=num_threads) as executor: + for _ in range(num_threads): + executor.submit(fib, n) + if num_threads > 1: + print(f"Running {num_threads} threads...", end="", flush=True) + else: + print("Running 1 thread...", end="", flush=True) + + +def fib(n): + return n if n < 2 else fib(n - 2) + fib(n - 1) + + +if __name__ == "__main__": + main(parse_args()) diff --git a/python-313/free-threading-jit/benchmarks/jit.py b/python-313/free-threading-jit/benchmarks/jit.py new file mode 100644 index 0000000000..b00c9fcb21 --- /dev/null +++ b/python-313/free-threading-jit/benchmarks/jit.py @@ -0,0 +1,76 @@ +from argparse import ArgumentParser +from csv import DictWriter +from functools import wraps +from pathlib import Path +from time import perf_counter +from typing import NamedTuple + +from pyfeatures import JitCompiler +from pyinfo import print_details, python_short + +CSV_PATH = Path(__file__).with_suffix(".csv") + + +class Record(NamedTuple): + python: str + jit: str + n: int + seconds: float + + def save(self): + empty = not CSV_PATH.exists() + with CSV_PATH.open(mode="a", encoding="utf-8", newline="") as file: + writer = DictWriter(file, Record._fields) + if empty: + writer.writeheader() + writer.writerow(self._asdict()) + + +def parse_args(): + parser = ArgumentParser() + parser.add_argument("-n", type=int, required=True) + return parser.parse_args() + + +def main(args): + print_details() + benchmark(args.n) + + +def timed(function): + jit = JitCompiler() + + @wraps(function) + def wrapper(n): + t1 = perf_counter() + result = function(n) + t2 = perf_counter() + duration = t2 - t1 + print(f"\b\b\b: {duration:.2f}s") + if jit.supported: + Record( + python_short(), "on" if jit.enabled else "off", n, duration + ).save() + else: + Record(python_short(), "unsupported", n, duration).save() + return result + + return wrapper + + +@timed +def benchmark(n): + print(f"Running fib() {n:,} times...", end="", flush=True) + for i in range(n): + fib(i) + + +def fib(n): + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a + + +if __name__ == "__main__": + main(parse_args()) diff --git a/python-313/free-threading-jit/benchmarks/pyfeatures.py b/python-313/free-threading-jit/benchmarks/pyfeatures.py new file mode 100644 index 0000000000..995680f84d --- /dev/null +++ b/python-313/free-threading-jit/benchmarks/pyfeatures.py @@ -0,0 +1,64 @@ +import abc +import sys +import sysconfig + +import _testinternalcapi + + +class Feature(abc.ABC): + def __init__(self, name: str) -> None: + self.name = name + + def __str__(self) -> str: + if self.supported: + if self.enabled: + return f"{self.name}: enabled \N{SPARKLES}" + else: + return f"{self.name}: disabled" + else: + return f"{self.name}: unsupported" + + @property + @abc.abstractmethod + def supported(self) -> bool: + pass + + @property + @abc.abstractmethod + def enabled(self) -> bool: + pass + + +class FreeThreading(Feature): + def __init__(self) -> None: + super().__init__("Free Threading") + + @property + def supported(self) -> bool: + return sysconfig.get_config_var("Py_GIL_DISABLED") == 1 + + @property + def enabled(self) -> bool: + return not self.gil_enabled() + + def gil_enabled(self) -> bool: + if sys.version_info >= (3, 13): + return sys._is_gil_enabled() + else: + return True + + +class JitCompiler(Feature): + def __init__(self): + super().__init__("JIT Compiler") + + @property + def supported(self) -> bool: + return "_Py_JIT" in sysconfig.get_config_var("PY_CORE_CFLAGS") + + @property + def enabled(self) -> bool: + if sys.version_info >= (3, 13): + return _testinternalcapi.get_optimizer() is not None + else: + return False diff --git a/python-313/free-threading-jit/benchmarks/pyinfo.py b/python-313/free-threading-jit/benchmarks/pyinfo.py new file mode 100644 index 0000000000..0dce3cf30a --- /dev/null +++ b/python-313/free-threading-jit/benchmarks/pyinfo.py @@ -0,0 +1,49 @@ +import os +import platform +import sys + +from pyfeatures import FreeThreading, JitCompiler + + +def print_details(): + lines = [ + system_details(), + python_details(), + str(FreeThreading()), + str(JitCompiler()), + ] + print(header := "=" * max(map(len, lines))) + print("\n".join(lines)) + print(header) + + +def system_details(): + name = platform.system() + arch, _ = platform.architecture() + cpu = platform.processor() + cores = os.cpu_count() + endian = f"{sys.byteorder} Endian".title() + return ( + f"\N{PERSONAL COMPUTER} {name} {arch} with " + f"{cores}x CPU cores ({cpu} {endian})" + ) + + +def python_details(): + implementation = platform.python_implementation() + version = platform.python_version() + path = sys.executable + return f"\N{SNAKE} {implementation} {version} {path}" + + +def python_short(): + version = ".".join(map(str, sys.version_info[:2])) + abi = sys.abiflags + ft = FreeThreading() + if ft.supported: + return f"{version}{abi} (GIL {'off' if ft.enabled else 'on'})" + return f"{version}{abi}" + + +if __name__ == "__main__": + print_details() diff --git a/python-313/free-threading-jit/benchmarks/uops.py b/python-313/free-threading-jit/benchmarks/uops.py new file mode 100644 index 0000000000..4df65c94b9 --- /dev/null +++ b/python-313/free-threading-jit/benchmarks/uops.py @@ -0,0 +1,30 @@ +import dis + +import _opcode +from pyinfo import print_details + + +def reveal_code(function): + if uops := "\n".join(_get_micro_ops(function)): + print(uops) + else: + print("Micro-ops unavailable") + + +def _get_micro_ops(function): + for executor in _get_executors(function): + for uop, *_ in executor: + yield uop + + +def _get_executors(function): + bytecode = function.__code__._co_code_adaptive + for offset in range(0, len(bytecode), 2): + if dis.opname[bytecode[offset]] == "ENTER_EXECUTOR": + try: + yield _opcode.get_executor(function.__code__, offset) + except ValueError: + pass + + +print_details() diff --git a/python-313/free-threading-jit/c_extension/greeter.c b/python-313/free-threading-jit/c_extension/greeter.c new file mode 100644 index 0000000000..9c42931691 --- /dev/null +++ b/python-313/free-threading-jit/c_extension/greeter.c @@ -0,0 +1,28 @@ +#include + +static PyObject* greeter_greet(PyObject* self, PyObject* args) { + const char *name = "anonymous"; + + if (!PyArg_ParseTuple(args, "|s", &name)) { + return NULL; + } + + return PyUnicode_FromFormat("Hello, %s!", name); +} + +static PyMethodDef greeter_methods[] = { + {"greet", greeter_greet, METH_VARARGS, "Greet someone"}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef greeter = { + PyModuleDef_HEAD_INIT, + "greeter", + "Greeting module", + -1, + greeter_methods +}; + +PyMODINIT_FUNC PyInit_greeter(void) { + return PyModule_Create(&greeter); +} diff --git a/python-313/free-threading-jit/c_extension/greeter_threaded.c b/python-313/free-threading-jit/c_extension/greeter_threaded.c new file mode 100644 index 0000000000..2b432193f1 --- /dev/null +++ b/python-313/free-threading-jit/c_extension/greeter_threaded.c @@ -0,0 +1,39 @@ +#include + +static PyObject* greeter_greet(PyObject* self, PyObject* args) { + const char *name = "anonymous"; + + if (!PyArg_ParseTuple(args, "|s", &name)) { + return NULL; + } + + return PyUnicode_FromFormat("Hello, %s!", name); +} + +static PyMethodDef greeter_methods[] = { + {"greet", greeter_greet, METH_VARARGS, "Greet someone"}, + {NULL, NULL, 0, NULL} +}; + +static int greeter_exec(PyObject *module) { + return 0; +} + +static PyModuleDef_Slot greeter_slots[] = { + {Py_mod_exec, greeter_exec}, + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {0, NULL} +}; + +static struct PyModuleDef greeter = { + PyModuleDef_HEAD_INIT, + "greeter", + "Greeting module", + 0, + greeter_methods, + greeter_slots, +}; + +PyMODINIT_FUNC PyInit_greeter(void) { + return PyModuleDef_Init(&greeter); +} diff --git a/python-313/free-threading-jit/csv_data/gil_i5-1035G7.csv b/python-313/free-threading-jit/csv_data/gil_i5-1035G7.csv new file mode 100644 index 0000000000..47ae5b45e0 --- /dev/null +++ b/python-313/free-threading-jit/csv_data/gil_i5-1035G7.csv @@ -0,0 +1,206 @@ +python,threads,seconds +3.12,1,1.3134276199998567 +3.12,1,1.3196520090004924 +3.12,1,1.346437557000172 +3.12,1,1.3664149180003733 +3.12,1,1.4048051679992568 +3.12,2,2.694117006999477 +3.12,2,2.7276749100001325 +3.12,2,2.7314533869994193 +3.12,2,2.7329147159998683 +3.12,2,2.7397803569992902 +3.12,3,4.267035730999851 +3.12,3,4.280974548999438 +3.12,3,4.298704393000662 +3.12,3,4.502919975000623 +3.12,3,4.5043496730004335 +3.12,4,5.638877871000659 +3.12,4,5.673995243999343 +3.12,4,5.693588591999287 +3.12,4,5.727499272999921 +3.12,4,5.7319446569999855 +3.12,5,7.14902475999952 +3.12,5,7.149204137999732 +3.12,5,7.151267313000062 +3.12,5,7.154334310000195 +3.12,5,7.187086854999507 +3.12,6,8.610447965000276 +3.12,6,8.61191246099952 +3.12,6,8.620227477999833 +3.12,6,8.632551812999736 +3.12,6,8.632705171000453 +3.12,6,8.663058817999627 +3.12,7,10.078614136000397 +3.12,7,10.093410888999642 +3.12,7,10.094883253000262 +3.12,7,10.101211767999303 +3.12,7,10.118719738999971 +3.12,7,10.150747581999894 +3.12,7,10.336162818000048 +3.12,8,11.541422926999985 +3.12,8,11.564770959999805 +3.12,8,11.609210489999896 +3.12,8,11.622581604000516 +3.12,8,11.635595108000416 +3.12,8,12.066468428000007 +3.12,9,12.969964461000018 +3.12,9,13.002648764000696 +3.12,9,13.010754177999843 +3.12,9,13.020311570000558 +3.12,9,13.021345156999814 +3.12,9,13.023173079000117 +3.12,10,14.488305717000003 +3.12,10,14.49359623700002 +3.12,10,14.495486980999885 +3.12,10,14.506897097999172 +3.12,10,14.79880942499949 +3.13,1,1.4526056029999381 +3.13,1,1.4910447820002446 +3.13,1,1.5054411490000348 +3.13,1,1.5144787310000538 +3.13,1,1.5374934010005745 +3.13,2,3.0017200230004164 +3.13,2,3.029852009000024 +3.13,2,3.0450959429999784 +3.13,2,3.148334801000601 +3.13,2,3.167654955999751 +3.13,3,4.656559494999783 +3.13,3,4.659111862999453 +3.13,3,4.685519294999722 +3.13,3,4.704519732000335 +3.13,3,4.865718848000142 +3.13,4,6.290163580999433 +3.13,4,6.35877016699942 +3.13,4,6.392505363000055 +3.13,4,6.430669286000011 +3.13,4,6.488557364999906 +3.13,5,7.946050973999263 +3.13,5,7.978180582000277 +3.13,5,7.98326726400046 +3.13,5,8.105274438999913 +3.13,5,8.205051593000462 +3.13,6,9.595758438999837 +3.13,6,9.735328490000029 +3.13,6,9.736451718999888 +3.13,6,9.79467002399997 +3.13,6,9.828504617999897 +3.13,7,11.510324642999876 +3.13,7,11.5550013940001 +3.13,7,11.561105655000574 +3.13,7,11.580141344999902 +3.13,7,11.602023164999991 +3.13,8,12.77249473900065 +3.13,8,12.856835366999803 +3.13,8,12.874518737000471 +3.13,8,12.922773117999895 +3.13,8,12.971025721999467 +3.13,9,14.520499091000602 +3.13,9,14.5806800750006 +3.13,9,14.607956961000127 +3.13,9,14.782848782000656 +3.13,9,14.796534383000107 +3.13,10,16.003543601000274 +3.13,10,16.017133258000285 +3.13,10,16.126939015999596 +3.13,10,16.209595586999967 +3.13,10,16.218572156000846 +3.13t (GIL on),1,2.038599731999966 +3.13t (GIL on),1,2.056179565000093 +3.13t (GIL on),1,2.0675729940003293 +3.13t (GIL on),1,2.1245576999999685 +3.13t (GIL on),1,2.2048983199993017 +3.13t (GIL on),2,4.209725992000131 +3.13t (GIL on),2,4.2235445680007615 +3.13t (GIL on),2,4.25305517400011 +3.13t (GIL on),2,4.340959334999752 +3.13t (GIL on),2,4.5538247610002145 +3.13t (GIL on),3,6.57306577899999 +3.13t (GIL on),3,6.705166627999461 +3.13t (GIL on),3,6.784167087999776 +3.13t (GIL on),3,6.892661120000412 +3.13t (GIL on),3,7.244368518000556 +3.13t (GIL on),4,8.737662663000265 +3.13t (GIL on),4,8.897516232999806 +3.13t (GIL on),4,9.00250199600032 +3.13t (GIL on),4,9.023183520999737 +3.13t (GIL on),4,9.072245833000125 +3.13t (GIL on),5,11.103883413000403 +3.13t (GIL on),5,11.119462065999869 +3.13t (GIL on),5,11.13127116600026 +3.13t (GIL on),5,11.14141154400022 +3.13t (GIL on),5,11.306006043000707 +3.13t (GIL on),6,13.328507397000067 +3.13t (GIL on),6,13.402858037999977 +3.13t (GIL on),6,13.553388061999613 +3.13t (GIL on),6,13.731452746000286 +3.13t (GIL on),6,13.896151584000108 +3.13t (GIL on),7,15.680445972000598 +3.13t (GIL on),7,15.717252460999589 +3.13t (GIL on),7,15.85188278500027 +3.13t (GIL on),7,16.143847409000045 +3.13t (GIL on),7,16.314354260999608 +3.13t (GIL on),8,18.04567834199952 +3.13t (GIL on),8,18.05228295300003 +3.13t (GIL on),8,18.133119623999846 +3.13t (GIL on),8,18.21577104700009 +3.13t (GIL on),8,18.678418815999976 +3.13t (GIL on),9,20.307986410000012 +3.13t (GIL on),9,20.36015075799969 +3.13t (GIL on),9,20.385711353999795 +3.13t (GIL on),9,20.6402318370001 +3.13t (GIL on),9,20.802998488000412 +3.13t (GIL on),10,22.638807311000164 +3.13t (GIL on),10,22.68675758400059 +3.13t (GIL on),10,22.96058408800036 +3.13t (GIL on),10,23.29116649200023 +3.13t (GIL on),10,24.086142801999813 +3.13t (GIL off),1,2.059381487999417 +3.13t (GIL off),1,2.0641737679998187 +3.13t (GIL off),1,2.070348236999962 +3.13t (GIL off),1,2.0734908880003786 +3.13t (GIL off),1,2.082689040000332 +3.13t (GIL off),2,2.117892256000232 +3.13t (GIL off),2,2.152980930999547 +3.13t (GIL off),2,2.2525489939998806 +3.13t (GIL off),2,2.334080655999969 +3.13t (GIL off),2,2.3407114729998284 +3.13t (GIL off),3,2.5811502189999374 +3.13t (GIL off),3,2.5893896980005593 +3.13t (GIL off),3,2.634486974000538 +3.13t (GIL off),3,2.6681225160000395 +3.13t (GIL off),3,2.6960706249992654 +3.13t (GIL off),4,3.0001550760007376 +3.13t (GIL off),4,3.1477478340002563 +3.13t (GIL off),4,3.1803885019999143 +3.13t (GIL off),4,3.216378948000056 +3.13t (GIL off),4,3.3545062800003507 +3.13t (GIL off),5,3.4861024630008615 +3.13t (GIL off),5,3.5134693270010757 +3.13t (GIL off),5,3.55424309099908 +3.13t (GIL off),5,3.576789407999968 +3.13t (GIL off),5,3.5899675870005012 +3.13t (GIL off),6,3.8138669279996975 +3.13t (GIL off),6,3.830776521000189 +3.13t (GIL off),6,3.9013560710000093 +3.13t (GIL off),6,3.948313586999575 +3.13t (GIL off),6,3.977262274000168 +3.13t (GIL off),7,4.233502478999981 +3.13t (GIL off),7,4.260773615000289 +3.13t (GIL off),7,4.263720668999667 +3.13t (GIL off),7,4.319025931000397 +3.13t (GIL off),7,4.3326507720003065 +3.13t (GIL off),8,4.886916605999431 +3.13t (GIL off),8,4.8958097729992005 +3.13t (GIL off),8,4.912695875000281 +3.13t (GIL off),8,4.922559956001351 +3.13t (GIL off),8,4.940515975999006 +3.13t (GIL off),9,5.603638077998767 +3.13t (GIL off),9,5.608411306000562 +3.13t (GIL off),9,5.6130411960002675 +3.13t (GIL off),9,5.637440939000044 +3.13t (GIL off),9,5.674600311000177 +3.13t (GIL off),10,6.172665352998592 +3.13t (GIL off),10,6.195153050000954 +3.13t (GIL off),10,6.2652514789988345 +3.13t (GIL off),10,6.28074031199867 +3.13t (GIL off),10,6.284552039999653 diff --git a/python-313/free-threading-jit/csv_data/gil_i5-4570S.csv b/python-313/free-threading-jit/csv_data/gil_i5-4570S.csv new file mode 100644 index 0000000000..a29734141d --- /dev/null +++ b/python-313/free-threading-jit/csv_data/gil_i5-4570S.csv @@ -0,0 +1,201 @@ +python,threads,seconds +3.12,1,1.5232975100002477 +3.12,1,1.5152002730001186 +3.12,1,1.5093664769997304 +3.12,1,1.5102836619998925 +3.12,1,1.5141319659996952 +3.12,2,3.170594612000059 +3.12,2,3.167410438999923 +3.12,2,3.1841503089999605 +3.12,2,3.189135288000216 +3.12,2,3.336082341000292 +3.12,3,4.869438209999771 +3.12,3,4.902084120999916 +3.12,3,4.868864010999914 +3.12,3,5.143975595000029 +3.12,3,5.153054969000095 +3.12,4,6.991564450999704 +3.12,4,6.77579274000027 +3.12,4,6.946960171999763 +3.12,4,6.838164402000075 +3.12,4,6.886344981000093 +3.12,5,8.355532738999955 +3.12,5,8.643900620999375 +3.12,5,8.44884121999985 +3.12,5,8.442169003000345 +3.12,5,8.634912126000017 +3.12,6,10.192847919000087 +3.12,6,10.570352777000153 +3.12,6,10.364811161000034 +3.12,6,10.269238099000177 +3.12,6,10.419044133999705 +3.12,7,12.154497258000447 +3.12,7,12.435035720999622 +3.12,7,12.056603042999996 +3.12,7,12.436911310999676 +3.12,7,12.51670318000015 +3.12,8,13.878770345999328 +3.12,8,14.02550836699993 +3.12,8,13.736038105000262 +3.12,8,14.410847880999427 +3.12,8,14.718670563999694 +3.12,9,15.478375365999455 +3.12,9,16.205163347000052 +3.12,9,15.958408841000164 +3.12,9,15.493583411000145 +3.12,9,15.807245262999459 +3.12,10,18.09663851700043 +3.12,10,17.667385165999804 +3.12,10,17.877299729000697 +3.12,10,17.554315546999533 +3.12,10,17.554402442000537 +3.13,1,1.561747123999794 +3.13,1,1.562759018000179 +3.13,1,1.5617610940003033 +3.13,1,1.560544236000169 +3.13,1,1.5674175130002368 +3.13,2,3.5071269690001827 +3.13,2,3.2589372039997215 +3.13,2,3.3407667559999936 +3.13,2,3.385114064999925 +3.13,2,3.2989410159998442 +3.13,3,5.101223997000034 +3.13,3,5.051788232000035 +3.13,3,5.1156283330001315 +3.13,3,5.093960451999919 +3.13,3,5.064664527000332 +3.13,4,6.854811000000154 +3.13,4,6.844086958999924 +3.13,4,6.8015537390001555 +3.13,4,6.850542841000333 +3.13,4,6.764329403999909 +3.13,5,8.670427138999912 +3.13,5,8.630183572000078 +3.13,5,8.53583493699989 +3.13,5,8.55562391300009 +3.13,5,8.571972575000018 +3.13,6,10.39753192499984 +3.13,6,10.3440590109999 +3.13,6,10.291454918 +3.13,6,10.325381320999895 +3.13,6,10.414761610000369 +3.13,7,12.123231669999768 +3.13,7,12.228487938999933 +3.13,7,12.18473633199983 +3.13,7,12.242742549000013 +3.13,7,12.047462686000017 +3.13,8,13.912882652000008 +3.13,8,13.89692600699982 +3.13,8,13.747307841000293 +3.13,8,13.741394902000138 +3.13,8,13.96930923500031 +3.13,9,15.566387119999945 +3.13,9,15.955968615000074 +3.13,9,16.00366906699992 +3.13,9,16.444538691999696 +3.13,9,16.498693946999992 +3.13,10,18.689670085000216 +3.13,10,18.574464167000315 +3.13,10,18.751912253000228 +3.13,10,18.485264801000085 +3.13,10,18.5132218509998 +3.13t (GIL on),1,2.739032135000116 +3.13t (GIL on),1,2.7624970679999024 +3.13t (GIL on),1,2.8928019690001747 +3.13t (GIL on),1,2.7550767330001236 +3.13t (GIL on),1,2.803708598999947 +3.13t (GIL on),2,5.903119785999934 +3.13t (GIL on),2,5.825038665999955 +3.13t (GIL on),2,6.1509773520001545 +3.13t (GIL on),2,6.174038293999956 +3.13t (GIL on),2,6.227957297000103 +3.13t (GIL on),3,9.663693698000088 +3.13t (GIL on),3,8.915744964000169 +3.13t (GIL on),3,8.789077284999848 +3.13t (GIL on),3,9.446435745999906 +3.13t (GIL on),3,8.905956820000029 +3.13t (GIL on),4,12.309754524000027 +3.13t (GIL on),4,11.746752486000105 +3.13t (GIL on),4,11.856715587000053 +3.13t (GIL on),4,13.361075106000044 +3.13t (GIL on),4,12.05081944099993 +3.13t (GIL on),5,15.610127939999984 +3.13t (GIL on),5,15.309576466999943 +3.13t (GIL on),5,15.365104688999963 +3.13t (GIL on),5,15.224238577000051 +3.13t (GIL on),5,15.634923783999966 +3.13t (GIL on),6,20.651044615000046 +3.13t (GIL on),6,18.21804096599999 +3.13t (GIL on),6,18.54383027799986 +3.13t (GIL on),6,18.497443638999812 +3.13t (GIL on),6,18.36984780900002 +3.13t (GIL on),7,21.486201963999974 +3.13t (GIL on),7,22.10737254600008 +3.13t (GIL on),7,22.91534850900007 +3.13t (GIL on),7,21.94229474399981 +3.13t (GIL on),7,22.294690318999983 +3.13t (GIL on),8,25.378065698000228 +3.13t (GIL on),8,24.778433492000204 +3.13t (GIL on),8,25.944585938999808 +3.13t (GIL on),8,25.356897070000286 +3.13t (GIL on),8,25.974200608000046 +3.13t (GIL on),9,27.630805321000025 +3.13t (GIL on),9,28.64921027699984 +3.13t (GIL on),9,28.86109836600008 +3.13t (GIL on),9,27.50763836599981 +3.13t (GIL on),9,27.807412619999923 +3.13t (GIL on),10,30.65068920700014 +3.13t (GIL on),10,31.842539717999898 +3.13t (GIL on),10,32.866430589000174 +3.13t (GIL on),10,32.580645945000015 +3.13t (GIL on),10,31.781087928000034 +3.13t (GIL off),1,2.7692627930000526 +3.13t (GIL off),1,3.361421041000085 +3.13t (GIL off),1,2.8249269379998623 +3.13t (GIL off),1,2.7714183529999445 +3.13t (GIL off),1,2.896101316000113 +3.13t (GIL off),1,2.859639197999968 +3.13t (GIL off),2,2.8654898019999564 +3.13t (GIL off),2,3.02152893199991 +3.13t (GIL off),2,3.133002592000139 +3.13t (GIL off),2,2.859638845999825 +3.13t (GIL off),2,2.8957070280000607 +3.13t (GIL off),3,3.1013295669999934 +3.13t (GIL off),3,3.0848869020001075 +3.13t (GIL off),3,3.0822611490000327 +3.13t (GIL off),3,3.0578516079999645 +3.13t (GIL off),4,3.1188252369997826 +3.13t (GIL off),4,3.1199177250000503 +3.13t (GIL off),4,3.029768541999829 +3.13t (GIL off),4,3.129524722000042 +3.13t (GIL off),5,3.854465161999997 +3.13t (GIL off),5,4.014284378999946 +3.13t (GIL off),5,3.908708615000023 +3.13t (GIL off),5,4.204316107000068 +3.13t (GIL off),5,4.191624549999915 +3.13t (GIL off),6,4.97330643600003 +3.13t (GIL off),6,5.080224080999869 +3.13t (GIL off),6,4.8829973760000485 +3.13t (GIL off),6,5.304110129999799 +3.13t (GIL off),6,4.928958264999892 +3.13t (GIL off),6,4.951369027000055 +3.13t (GIL off),7,5.502766806999944 +3.13t (GIL off),7,5.413134285000069 +3.13t (GIL off),7,5.688517767000121 +3.13t (GIL off),7,5.816813075000027 +3.13t (GIL off),7,5.824078767999936 +3.13t (GIL off),8,6.25018295100017 +3.13t (GIL off),8,6.049671135999915 +3.13t (GIL off),8,6.235134628999958 +3.13t (GIL off),8,6.274629306999941 +3.13t (GIL off),8,6.03818370099998 +3.13t (GIL off),9,6.8176364440000725 +3.13t (GIL off),9,6.842499166000152 +3.13t (GIL off),9,7.260077315999979 +3.13t (GIL off),9,7.298692024000047 +3.13t (GIL off),9,7.345396875000006 +3.13t (GIL off),10,7.577900948999968 +3.13t (GIL off),10,8.273929427999974 +3.13t (GIL off),10,8.08283532900009 +3.13t (GIL off),10,7.748553399000002 +3.13t (GIL off),10,7.882627834999994 diff --git a/python-313/free-threading-jit/csv_data/gil_i7-6600U.csv b/python-313/free-threading-jit/csv_data/gil_i7-6600U.csv new file mode 100644 index 0000000000..5e0b80b93a --- /dev/null +++ b/python-313/free-threading-jit/csv_data/gil_i7-6600U.csv @@ -0,0 +1,209 @@ +python,threads,seconds +3.12,1,1.6167850980018557 +3.12,1,1.5893613999978697 +3.12,1,1.595783204997133 +3.12,1,1.6229920300029335 +3.12,1,1.601877355002216 +3.12,2,3.273815612999897 +3.12,2,3.264639682998677 +3.12,2,3.2814007279994257 +3.12,2,3.2967408069998783 +3.12,2,3.3097324640002626 +3.12,3,4.959194002000004 +3.12,3,4.9826916990023165 +3.12,3,4.989614348000032 +3.12,3,4.987466792001214 +3.12,3,4.95907954300128 +3.12,4,6.620533527999214 +3.12,4,6.572718556999462 +3.12,4,6.643193636002252 +3.12,4,6.593654857999354 +3.12,4,6.631763372999558 +3.12,5,8.34123701399949 +3.12,5,8.131208990002051 +3.12,5,8.135288625999237 +3.12,5,8.115088606999052 +3.12,5,8.144278711999505 +3.12,6,9.791196499001671 +3.12,6,9.813760871998966 +3.12,6,9.810381950999727 +3.12,6,9.786202309001965 +3.12,6,9.821494738000183 +3.12,7,11.428098647000297 +3.12,7,11.454482247998385 +3.12,7,11.423438891000842 +3.12,7,11.429158738999831 +3.12,7,11.494361638000555 +3.12,7,11.442028920002485 +3.12,7,11.459264855002402 +3.12,7,11.434507271002076 +3.12,8,13.154986609999469 +3.12,8,13.137387013000989 +3.12,8,13.149587915002485 +3.12,8,13.141267931001494 +3.12,8,13.131569040000613 +3.12,9,14.818584683998779 +3.12,9,14.779611583999213 +3.12,9,14.85035267800049 +3.12,9,14.936020178000035 +3.12,9,14.888629496999783 +3.12,10,16.494835818000865 +3.12,10,16.52170243100045 +3.12,10,16.383074032997683 +3.12,10,16.468056420999346 +3.12,10,16.438899663000484 +3.13,1,1.506625609999901 +3.13,1,1.528135234999354 +3.13,1,1.535411019998719 +3.13,1,1.503571674998966 +3.13,1,1.5089633610004967 +3.13,2,3.130032374003349 +3.13,2,3.1285214079980506 +3.13,2,3.1361670730002515 +3.13,2,3.1453136979980627 +3.13,2,3.1394223559982493 +3.13,3,4.7700741730004665 +3.13,3,4.7029337549975025 +3.13,3,4.748352433001855 +3.13,3,4.7391729400005715 +3.13,3,4.75732962300026 +3.13,4,6.396649411002727 +3.13,4,6.2773965380001755 +3.13,4,6.316844131000835 +3.13,4,6.304177474998141 +3.13,4,6.326903624001716 +3.13,5,7.949291795001045 +3.13,5,7.944259741998394 +3.13,5,7.980765918000543 +3.13,5,7.94873058200028 +3.13,5,7.970075637000264 +3.13,6,9.512840406001487 +3.13,6,9.518236845000501 +3.13,6,9.573679755001649 +3.13,6,9.518751323001197 +3.13,6,9.604141036001238 +3.13,7,11.167755703001603 +3.13,7,11.212075471001299 +3.13,7,11.187995849999425 +3.13,7,11.17050822300007 +3.13,7,11.19935866599917 +3.13,7,11.20193208099954 +3.13,8,12.848266519999015 +3.13,8,12.788366952001525 +3.13,8,12.817743638999673 +3.13,8,12.700774861001264 +3.13,8,12.767423923000024 +3.13,9,14.407486116000655 +3.13,9,14.41260710899951 +3.13,9,14.361858000000211 +3.13,9,14.401381614999991 +3.13,9,14.40556186899994 +3.13,10,15.972966894001729 +3.13,10,15.998240562999854 +3.13,10,16.127217190998635 +3.13,10,16.062733310998738 +3.13,10,16.059287408999808 +3.13,10,15.9893913709966 +3.13,10,16.041180330001225 +3.13t (GIL on),1,2.745885600001202 +3.13t (GIL on),1,2.686686897999607 +3.13t (GIL on),1,2.671247708996816 +3.13t (GIL on),1,2.846670095997979 +3.13t (GIL on),1,2.646512110000913 +3.13t (GIL on),2,5.442764409999654 +3.13t (GIL on),2,5.598778494000726 +3.13t (GIL on),2,5.591584707002767 +3.13t (GIL on),2,5.419537662000948 +3.13t (GIL on),2,5.447461829997337 +3.13t (GIL on),2,5.738007889001892 +3.13t (GIL on),3,8.311606605002453 +3.13t (GIL on),3,8.229199820001668 +3.13t (GIL on),3,8.916051465999772 +3.13t (GIL on),3,8.234101655001723 +3.13t (GIL on),3,8.452070456998626 +3.13t (GIL on),4,11.020135630998993 +3.13t (GIL on),4,11.006001703997754 +3.13t (GIL on),4,12.999260219999996 +3.13t (GIL on),4,11.309371262999775 +3.13t (GIL on),4,11.004826288000913 +3.13t (GIL on),4,10.997436123001535 +3.13t (GIL on),5,13.80695640400154 +3.13t (GIL on),5,14.209277286001452 +3.13t (GIL on),5,13.82891073599967 +3.13t (GIL on),5,13.88259122800082 +3.13t (GIL on),5,13.847790451000037 +3.13t (GIL on),6,17.15798933699989 +3.13t (GIL on),6,17.60821695299819 +3.13t (GIL on),6,16.631253813000512 +3.13t (GIL on),6,16.64622228100052 +3.13t (GIL on),6,17.54251532499984 +3.13t (GIL on),7,20.067411515999993 +3.13t (GIL on),7,19.43176555899845 +3.13t (GIL on),7,20.46718123499886 +3.13t (GIL on),7,19.47254171199893 +3.13t (GIL on),7,19.440473643000587 +3.13t (GIL on),8,25.739889353000763 +3.13t (GIL on),8,22.29233595199912 +3.13t (GIL on),8,22.27077645600002 +3.13t (GIL on),8,22.29204866999862 +3.13t (GIL on),8,22.200236145999952 +3.13t (GIL on),9,25.256618040999456 +3.13t (GIL on),9,25.156509831998846 +3.13t (GIL on),9,25.813895191000483 +3.13t (GIL on),9,25.85894351800016 +3.13t (GIL on),9,25.097168207001232 +3.13t (GIL on),10,27.946848982999654 +3.13t (GIL on),10,28.022870633998537 +3.13t (GIL on),10,28.019629121001344 +3.13t (GIL on),10,30.23154010199869 +3.13t (GIL on),10,27.814480673998332 +3.13t (GIL off),1,2.82331756900021 +3.13t (GIL off),1,2.681639719001396 +3.13t (GIL off),1,2.7482051010010764 +3.13t (GIL off),1,2.759628428000724 +3.13t (GIL off),1,2.6455425700005435 +3.13t (GIL off),2,2.7707308960016235 +3.13t (GIL off),2,3.086538006002229 +3.13t (GIL off),2,2.806217989000288 +3.13t (GIL off),2,2.783274059998803 +3.13t (GIL off),2,2.8800539080002636 +3.13t (GIL off),3,4.329015246999916 +3.13t (GIL off),3,4.411699889998999 +3.13t (GIL off),3,4.471805144999962 +3.13t (GIL off),3,4.4337308409994876 +3.13t (GIL off),3,4.415077310000925 +3.13t (GIL off),4,6.6237843629969575 +3.13t (GIL off),4,6.289843498001574 +3.13t (GIL off),4,6.394446877999144 +3.13t (GIL off),4,6.126050966999173 +3.13t (GIL off),4,6.136650209002255 +3.13t (GIL off),5,7.886594389001402 +3.13t (GIL off),5,8.199138707997918 +3.13t (GIL off),5,7.651410878999741 +3.13t (GIL off),5,7.845159607000824 +3.13t (GIL off),5,7.870763350998459 +3.13t (GIL off),6,9.170307909000257 +3.13t (GIL off),6,9.216360187001555 +3.13t (GIL off),6,9.21952520099876 +3.13t (GIL off),6,9.634512069002085 +3.13t (GIL off),6,9.187128361001669 +3.13t (GIL off),7,10.719385762000456 +3.13t (GIL off),7,10.700220239999908 +3.13t (GIL off),7,10.836716426001658 +3.13t (GIL off),7,11.638157501998649 +3.13t (GIL off),7,10.69872907899844 +3.13t (GIL off),8,12.561636524002097 +3.13t (GIL off),8,12.25417365100293 +3.13t (GIL off),8,12.40116459100318 +3.13t (GIL off),8,12.251919330999954 +3.13t (GIL off),8,12.29102864000015 +3.13t (GIL off),9,13.81171505400198 +3.13t (GIL off),9,13.831427815999632 +3.13t (GIL off),9,13.854064039001969 +3.13t (GIL off),9,13.928756249999424 +3.13t (GIL off),9,13.95855801700236 +3.13t (GIL off),10,15.291116648000752 +3.13t (GIL off),10,15.68030551999982 +3.13t (GIL off),10,15.2975162440016 +3.13t (GIL off),10,15.292086945999472 +3.13t (GIL off),10,15.298041703001218 diff --git a/python-313/free-threading-jit/csv_data/jit_i5-4570S.csv b/python-313/free-threading-jit/csv_data/jit_i5-4570S.csv new file mode 100644 index 0000000000..56a94cc7ad --- /dev/null +++ b/python-313/free-threading-jit/csv_data/jit_i5-4570S.csv @@ -0,0 +1,136 @@ +python,jit,n,seconds +3.12,unsupported,500,0.009369454997795401 +3.12,unsupported,500,0.007353717002843041 +3.12,unsupported,500,0.006998583001404768 +3.12,unsupported,500,0.007619179999892367 +3.12,unsupported,500,0.00877314299941645 +3.12,unsupported,1000,0.031133521999436198 +3.12,unsupported,1000,0.0323545440005546 +3.12,unsupported,1000,0.03183418000116944 +3.12,unsupported,1000,0.031146034998528194 +3.12,unsupported,1000,0.03111951399841928 +3.12,unsupported,5000,1.1521984069986502 +3.12,unsupported,5000,1.1638271820011141 +3.12,unsupported,5000,1.15261974799796 +3.12,unsupported,5000,1.1694310819984821 +3.12,unsupported,5000,1.1625076089985669 +3.12,unsupported,10000,6.36163999800192 +3.12,unsupported,10000,6.338171537998278 +3.12,unsupported,10000,6.33395152400044 +3.12,unsupported,10000,6.7673945969982015 +3.12,unsupported,10000,6.372794914001133 +3.12,unsupported,15000,18.15166205100104 +3.12,unsupported,15000,18.44748627900117 +3.12,unsupported,15000,18.23540391699862 +3.12,unsupported,15000,18.1691022720006 +3.12,unsupported,15000,18.12873476999812 +3.12,unsupported,20000,39.916185784000845 +3.12,unsupported,20000,39.10055238399946 +3.12,unsupported,20000,39.10745589200087 +3.12,unsupported,20000,39.06687986600082 +3.12,unsupported,20000,40.067064240000036 +3.12,unsupported,30000,118.44751215999999 +3.12,unsupported,30000,123.14322116299999 +3.12,unsupported,30000,120.027523513 +3.12,unsupported,30000,121.24779327699997 +3.12,unsupported,30000,120.31818975500005 +3.13,unsupported,500,0.005844719999998915 +3.13,unsupported,500,0.006008251000025666 +3.13,unsupported,500,0.005726095999989411 +3.13,unsupported,500,0.005635914000009734 +3.13,unsupported,500,0.006052468999996563 +3.13,unsupported,1000,0.026009974000004377 +3.13,unsupported,1000,0.025930739999978414 +3.13,unsupported,1000,0.026180298999975093 +3.13,unsupported,1000,0.025545633999996653 +3.13,unsupported,1000,0.026703069999996387 +3.13,unsupported,5000,1.0303426090000016 +3.13,unsupported,5000,1.022278263000004 +3.13,unsupported,5000,1.0137221100000033 +3.13,unsupported,5000,1.029508270000008 +3.13,unsupported,5000,1.007983902999996 +3.13,unsupported,10000,5.790301991000007 +3.13,unsupported,10000,5.8135147920000065 +3.13,unsupported,10000,5.794255214000003 +3.13,unsupported,10000,5.803752430000003 +3.13,unsupported,10000,5.803060695999989 +3.13,unsupported,15000,16.90848988999997 +3.13,unsupported,15000,16.93269645099997 +3.13,unsupported,15000,16.921878643000014 +3.13,unsupported,15000,17.084298941000043 +3.13,unsupported,15000,16.881450298999994 +3.13,unsupported,20000,37.10813562300001 +3.13,unsupported,20000,37.10708452199998 +3.13,unsupported,20000,37.12908822899999 +3.13,unsupported,20000,37.05972656099999 +3.13,unsupported,20000,37.547475021000025 +3.13,off,500,0.00579837400073302 +3.13,off,500,0.006506813002488343 +3.13,off,500,0.00588250099826837 +3.13,off,500,0.006681314000161365 +3.13,off,500,0.0057909549977921415 +3.13,off,1000,0.025892514000588562 +3.13,off,1000,0.02633745999992243 +3.13,off,1000,0.02638619000208564 +3.13,off,1000,0.02554364500247175 +3.13,off,1000,0.026637132999894675 +3.13,off,5000,1.0050257449984201 +3.13,off,5000,1.0097730110028351 +3.13,off,5000,1.021126154002559 +3.13,off,5000,1.0117620229975728 +3.13,off,5000,1.0098318479977024 +3.13,off,10000,5.747700055002497 +3.13,off,10000,5.8320127469996805 +3.13,off,10000,5.781688287999714 +3.13,off,10000,5.784440583000105 +3.13,off,10000,5.797936356000719 +3.13,off,15000,16.8618842399992 +3.13,off,15000,16.855940622997878 +3.13,off,15000,16.880565868999838 +3.13,off,15000,16.848944581997785 +3.13,off,15000,17.16575363499942 +3.13,off,20000,37.55678654000076 +3.13,off,20000,36.95832089100077 +3.13,off,20000,36.98102991000269 +3.13,off,20000,36.9898522600015 +3.13,off,20000,38.0725489510005 +3.13,off,30000,116.13733132400012 +3.13,off,30000,116.78851730499991 +3.13,off,30000,116.09899993599902 +3.13,off,30000,114.01790211499974 +3.13,off,30000,116.06904446399858 +3.13,on,500,0.0054716880003979895 +3.13,on,500,0.005896999999094987 +3.13,on,500,0.005212731997744413 +3.13,on,500,0.005627610000374261 +3.13,on,500,0.005842896000103792 +3.13,on,1000,0.023059297000145307 +3.13,on,1000,0.022721842000464676 +3.13,on,1000,0.022886938000738155 +3.13,on,1000,0.02477407400147058 +3.13,on,1000,0.021413954000308877 +3.13,on,5000,0.8919412239993108 +3.13,on,5000,0.9226561130017217 +3.13,on,5000,0.9023900600004708 +3.13,on,5000,0.8861959220012068 +3.13,on,5000,0.8951090590016975 +3.13,on,10000,5.471404212999914 +3.13,on,10000,5.259307510998042 +3.13,on,10000,5.263253892000648 +3.13,on,10000,5.25580121999883 +3.13,on,10000,5.271517570003198 +3.13,on,15000,15.553052467999805 +3.13,on,15000,15.510835755998414 +3.13,on,15000,15.497080874996755 +3.13,on,15000,15.557670534999488 +3.13,on,15000,15.762930607997987 +3.13,on,20000,34.66291092699976 +3.13,on,20000,34.60241940000196 +3.13,on,20000,34.9378525050015 +3.13,on,20000,34.771317623999494 +3.13,on,20000,34.60064512499957 +3.13,on,30000,115.17620235099912 +3.13,on,30000,111.08065649899982 +3.13,on,30000,111.32822081300037 +3.13,on,30000,110.71809967599984 +3.13,on,30000,109.9566754059997 \ No newline at end of file diff --git a/python-313/repl/guessing_game.py b/python-313/repl/guessing_game.py new file mode 100644 index 0000000000..e4fb1af44f --- /dev/null +++ b/python-313/repl/guessing_game.py @@ -0,0 +1,18 @@ +import random + +print("Guess my secret number!\n") +max_value = int(input("Enter maximum for the secret number: ")) + +print(f"Guess my number between 1 and {max_value}") +secret = random.randint(1, max_value) + +while True: + guess = int(input("Guess a number: ")) + if secret < guess: + print(f"Sorry, my number is lower than {guess}") + elif secret > guess: + print(f"Sorry, my number is higher than {guess}") + else: + break + +print(f"Yes, my number is {secret}") diff --git a/python-313/repl/multiline_editing.py b/python-313/repl/multiline_editing.py new file mode 100644 index 0000000000..6c549ad244 --- /dev/null +++ b/python-313/repl/multiline_editing.py @@ -0,0 +1,10 @@ +# fmt: off + +numbers = range(3, 13) +cubes = [ + (number - 3) ** 3 for number in numbers + if number % 2 == 1 +] +# fmt: on + +print(cubes) diff --git a/python-313/repl/power_factory.py b/python-313/repl/power_factory.py new file mode 100644 index 0000000000..ed024b1077 --- /dev/null +++ b/python-313/repl/power_factory.py @@ -0,0 +1,12 @@ +class PowerFactory: + """Create instances that can calculate powers.""" + + def __init__(self, exponent): + self.exponent = exponent + + def __call__(self, number): + return number**self.exponent + + +cubed = PowerFactory(3) +print(cubed(13)) diff --git a/python-313/repl/tab_completion.py b/python-313/repl/tab_completion.py new file mode 100644 index 0000000000..7064bd2ed6 --- /dev/null +++ b/python-313/repl/tab_completion.py @@ -0,0 +1,8 @@ +import math + +print(math.cos(2 * math.pi)) + +headline = "python 3.13" +print(headline.title()) +print(headline.title().center(20)) +print(headline.title().center(20, "-")) From 69982e1830462f73c0bfb2c7c00090823a858dde Mon Sep 17 00:00:00 2001 From: gahjelle Date: Tue, 24 Sep 2024 13:48:05 +0200 Subject: [PATCH 20/29] TR update --- python-313/README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/python-313/README.md b/python-313/README.md index f9f092032c..4fba3e6257 100644 --- a/python-313/README.md +++ b/python-313/README.md @@ -1,6 +1,6 @@ # Python 3.13 Demos -This repository contains sample code and data files that demos some of the new features in Python 3.13. +This repository contains sample code and data files that demonstrate some of the new features in Python 3.13. ## Introduction @@ -24,8 +24,22 @@ This section only contains brief instructions on how you can run the examples. S ### REPL +The following examples are used to demonstrate different features of the new REPL: +- [`tab_completion.py`](repl/tab_completion.py) +- [`multiline_editing.py`](repl/multiline_editing.py) +- [`power_factory.py](repl/power_factory.py) +- [`guessing_game.py](repl/guessing_game.py) ### Free-Threading and JIT -You need to enable a few build options to try out the free-threading and JIT features in Python 3.13. You can find more information in the dedicated [README file](free-threading-jit/README.md). \ No newline at end of file +You need to enable a few build options to try out the free-threading and JIT features in Python 3.13. You can find more information in the dedicated [README file](free-threading-jit/README.md). + +## Authors + +- **Bartosz Zaczyński**, E-mail: [bartosz@realpython.com](bartosz@realpython.com) +- **Geir Arne Hjelle**, E-mail: [geirarne@realpython.com](geirarne@realpython.com) + +## License + +Distributed under the MIT license. See [`LICENSE`](../LICENSE) for more information. From cead9e46fe5abc9b2972cce19dacc7ca75a929e9 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 24 Sep 2024 17:06:03 +0200 Subject: [PATCH 21/29] TR updates, first round --- python-closure/app.py | 2 +- python-closure/{stack_class.py => stack_v1.py} | 0 python-closure/{stack_closure.py => stack_v2.py} | 10 ++++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) rename python-closure/{stack_class.py => stack_v1.py} (100%) rename python-closure/{stack_closure.py => stack_v2.py} (71%) diff --git a/python-closure/app.py b/python-closure/app.py index c9368b3824..bfa197df34 100644 --- a/python-closure/app.py +++ b/python-closure/app.py @@ -2,7 +2,7 @@ app = tk.Tk() app.title("GUI App") -app.geometry("200x60") +app.geometry("320x240") label = tk.Label(app, font=("Helvetica", 16, "bold")) label.pack() diff --git a/python-closure/stack_class.py b/python-closure/stack_v1.py similarity index 100% rename from python-closure/stack_class.py rename to python-closure/stack_v1.py diff --git a/python-closure/stack_closure.py b/python-closure/stack_v2.py similarity index 71% rename from python-closure/stack_closure.py rename to python-closure/stack_v2.py index 36c7bc664e..b0ee1b2c63 100644 --- a/python-closure/stack_closure.py +++ b/python-closure/stack_v2.py @@ -13,3 +13,13 @@ def closure(): closure.push = push closure.pop = pop return closure + + +stack = Stack() +stack.push(1) +stack.push(2) +stack.push(3) + +stack.pop() + +stack._items From 9d11980c6d786f2d4ea81897bf23ab6159bccbe3 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 24 Sep 2024 17:07:09 +0200 Subject: [PATCH 22/29] Remove usage code --- python-closure/stack_v2.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/python-closure/stack_v2.py b/python-closure/stack_v2.py index b0ee1b2c63..36c7bc664e 100644 --- a/python-closure/stack_v2.py +++ b/python-closure/stack_v2.py @@ -13,13 +13,3 @@ def closure(): closure.push = push closure.pop = pop return closure - - -stack = Stack() -stack.push(1) -stack.push(2) -stack.push(3) - -stack.pop() - -stack._items From a6c78247314096652f6c35eb7d90b40a2a3ff735 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Wed, 25 Sep 2024 13:20:08 +0200 Subject: [PATCH 23/29] TR updates, first round --- python-closure/closure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-closure/closure.py b/python-closure/closure.py index ce99e3f69e..65d7ef855a 100644 --- a/python-closure/closure.py +++ b/python-closure/closure.py @@ -9,7 +9,7 @@ def outer_func(): name = "Pythonista" - return lambda name=name: print(f"Hello, {name}!") + return lambda: print(f"Hello, {name}!") inner_func = outer_func() From e116b67eb0957950ffd0a4a553620d5e9d4e8587 Mon Sep 17 00:00:00 2001 From: gahjelle Date: Thu, 26 Sep 2024 14:22:52 +0200 Subject: [PATCH 24/29] Add examples from Python 3.13: Cool New Features --- python-313/README.md | 19 +++++- python-313/docstrings.py | 16 +++++ python-313/errors/inverse.py | 5 ++ python-313/errors/kwarg_suggest.py | 4 ++ python-313/errors/random.py | 14 +++++ python-313/music/opera/flower_duet.txt | 0 python-313/music/opera/habanera.txt | 0 python-313/music/opera/nabucco.txt | 0 python-313/music/rap/bedlam_13-13.txt | 0 python-313/music/rap/fight_the_power.txt | 0 python-313/paths.py | 27 ++++++++ python-313/repl/roll_dice.py | 14 +++++ python-313/replace.py | 23 +++++++ python-313/typing/deprecations.py | 79 ++++++++++++++++++++++++ python-313/typing/generic_queue.py | 38 ++++++++++++ python-313/typing/pyproject.toml | 2 + python-313/typing/readonly.py | 45 ++++++++++++++ python-313/typing/tree.py | 34 ++++++++++ 18 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 python-313/docstrings.py create mode 100644 python-313/errors/inverse.py create mode 100644 python-313/errors/kwarg_suggest.py create mode 100644 python-313/errors/random.py create mode 100644 python-313/music/opera/flower_duet.txt create mode 100644 python-313/music/opera/habanera.txt create mode 100644 python-313/music/opera/nabucco.txt create mode 100644 python-313/music/rap/bedlam_13-13.txt create mode 100644 python-313/music/rap/fight_the_power.txt create mode 100644 python-313/paths.py create mode 100644 python-313/repl/roll_dice.py create mode 100644 python-313/replace.py create mode 100644 python-313/typing/deprecations.py create mode 100644 python-313/typing/generic_queue.py create mode 100644 python-313/typing/pyproject.toml create mode 100644 python-313/typing/readonly.py create mode 100644 python-313/typing/tree.py diff --git a/python-313/README.md b/python-313/README.md index 4fba3e6257..5b99956f47 100644 --- a/python-313/README.md +++ b/python-313/README.md @@ -12,7 +12,7 @@ Note that for testing the free-threading and JIT features, you'll need to build You can learn more about Python 3.13's new features in the following Real Python tutorials: - +- [Python 3.13: Cool New Features for You to Try](https://realpython.com/python313-new-features/) - [Python 3.13 Preview: Free Threading and a JIT Compiler](https://realpython.com/python313-free-threading-jit/) - [Python 3.13 Preview: A Modern REPL](https://realpython.com/python313-repl) @@ -30,11 +30,28 @@ The following examples are used to demonstrate different features of the new REP - [`multiline_editing.py`](repl/multiline_editing.py) - [`power_factory.py](repl/power_factory.py) - [`guessing_game.py](repl/guessing_game.py) +- [`roll_dice.py`](repl/roll_dice.py) + +### Error messages + +Run the scripts in the `errors/` folder to see different error messages produced by Python 3.13. ### Free-Threading and JIT You need to enable a few build options to try out the free-threading and JIT features in Python 3.13. You can find more information in the dedicated [README file](free-threading-jit/README.md). +## Static typing + +Run the scripts in the `typing/` folder to try out the new static typing features. + +## Other features + +The following scripts illustrate other new features in Python 3.13: + +- [`replace.py`](replace.py): Use `copy.replace()` to update immutable data structures. +- [`paths.py`](paths.py) and [`music/`](music/): Glob patterns are more consistent. +- [`docstrings.py`](docstrings.py): Common leading whitespace in docstrings is stripped. + ## Authors - **Bartosz Zaczyński**, E-mail: [bartosz@realpython.com](bartosz@realpython.com) diff --git a/python-313/docstrings.py b/python-313/docstrings.py new file mode 100644 index 0000000000..3b25cc44ff --- /dev/null +++ b/python-313/docstrings.py @@ -0,0 +1,16 @@ +import dataclasses + + +@dataclasses.dataclass +class Person: + """Model a person with a name, location, and Python version.""" + + name: str + place: str + version: str + + +print(Person.__doc__) + +print(len(dataclasses.replace.__doc__)) +print(dataclasses.replace.__doc__) diff --git a/python-313/errors/inverse.py b/python-313/errors/inverse.py new file mode 100644 index 0000000000..5087c5e030 --- /dev/null +++ b/python-313/errors/inverse.py @@ -0,0 +1,5 @@ +def inverse(number): + return 1 / number + + +print(inverse(0)) diff --git a/python-313/errors/kwarg_suggest.py b/python-313/errors/kwarg_suggest.py new file mode 100644 index 0000000000..d5a99cfa89 --- /dev/null +++ b/python-313/errors/kwarg_suggest.py @@ -0,0 +1,4 @@ +numbers = [2, 0, 2, 4, 1, 0, 0, 1] + +# print(sorted(numbers, reversed=True)) +print(sorted(numbers, reverse=True)) diff --git a/python-313/errors/random.py b/python-313/errors/random.py new file mode 100644 index 0000000000..b129b13c10 --- /dev/null +++ b/python-313/errors/random.py @@ -0,0 +1,14 @@ +import random + +num_faces = 6 + +print("Hit enter to roll die (q to quit, number for # of faces) ") +while True: + roll = input() + if roll.lower().startswith("q"): + break + if roll.isnumeric(): + num_faces = int(roll) + + result = random.randint(1, num_faces) + print(f"Rolling a d{num_faces:<2d} - {result:2d}") diff --git a/python-313/music/opera/flower_duet.txt b/python-313/music/opera/flower_duet.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/opera/habanera.txt b/python-313/music/opera/habanera.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/opera/nabucco.txt b/python-313/music/opera/nabucco.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/rap/bedlam_13-13.txt b/python-313/music/rap/bedlam_13-13.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/rap/fight_the_power.txt b/python-313/music/rap/fight_the_power.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/paths.py b/python-313/paths.py new file mode 100644 index 0000000000..d3ed8b8ddc --- /dev/null +++ b/python-313/paths.py @@ -0,0 +1,27 @@ +import glob +import re +from pathlib import Path + +print('\nUsing glob("*"):\n') +for path in Path("music").glob("*"): + print(" ", path) + +print('\nUsing glob("**"):\n') +for path in Path("music").glob("**"): + print(" ", path) + +print('\nUsing glob("**/*"):\n') +for path in Path("music").glob("**/*"): + print(" ", path) + +print('\nUsing glob("**/"):\n') +for path in Path("music").glob("**/"): + print(" ", path) + +print("\nglob.translate()\n") +pattern = glob.translate("music/**/*.txt") +print(pattern) + +print(re.match(pattern, "music/opera/flower_duet.txt")) +print(re.match(pattern, "music/progressive_rock/")) +print(re.match(pattern, "music/progressive_rock/fandango.txt")) diff --git a/python-313/repl/roll_dice.py b/python-313/repl/roll_dice.py new file mode 100644 index 0000000000..b129b13c10 --- /dev/null +++ b/python-313/repl/roll_dice.py @@ -0,0 +1,14 @@ +import random + +num_faces = 6 + +print("Hit enter to roll die (q to quit, number for # of faces) ") +while True: + roll = input() + if roll.lower().startswith("q"): + break + if roll.isnumeric(): + num_faces = int(roll) + + result = random.randint(1, num_faces) + print(f"Rolling a d{num_faces:<2d} - {result:2d}") diff --git a/python-313/replace.py b/python-313/replace.py new file mode 100644 index 0000000000..00b994bcc1 --- /dev/null +++ b/python-313/replace.py @@ -0,0 +1,23 @@ +import copy +from datetime import date +from typing import NamedTuple + + +class Person(NamedTuple): + name: str + place: str + version: str + + +person = Person(name="Geir Arne", place="Oslo", version="3.12") +person = Person(name=person.name, place=person.place, version="3.13") +print(person) + +today = date.today() +print(today) +print(today.replace(day=1)) +print(today.replace(month=12, day=24)) + +person = Person(name="Geir Arne", place="Oslo", version="3.12") +print(copy.replace(person, version="3.13")) +print(copy.replace(today, day=1)) diff --git a/python-313/typing/deprecations.py b/python-313/typing/deprecations.py new file mode 100644 index 0000000000..9f8c3c2e4c --- /dev/null +++ b/python-313/typing/deprecations.py @@ -0,0 +1,79 @@ +"""Demonstration of PEP 702: Marking deprecations using the type system + +Use PyLance in VS Code by setting Python › Analysis: Type Checking Mode or run +the Pyright CLI: + + $ python -m pip install pyright $ pyright --pythonversion 3.13 . + +Note that showing warnings requires setting the reportDeprecated option in +Pyright. This is done in pyproject.toml. +""" + +from typing import overload +from warnings import deprecated + + +@deprecated("Use + instead of calling concatenate()") +def concatenate(first: str, second: str) -> str: + return first + second + + +@overload +@deprecated("add() is only supported for floats") +def add(x: int, y: int) -> int: ... +@overload +def add(x: float, y: float) -> float: ... + + +def add(x, y): + return x + y + + +class Version: + def __init__(self, major: int, minor: int = 0, patch: int = 0) -> None: + self.major = major + self.minor = minor + self.patch = patch + + @property + @deprecated("Use .patch instead") + def bugfix(self): + return self.patch + + def bump(self, part: str) -> None: + if part == "major": + self.major += 1 + self.minor = 0 + self.patch = 0 + elif part == "minor": + self.minor += 1 + self.patch = 0 + elif part == "patch": + self.patch += 1 + else: + raise ValueError("part must be 'major', 'minor', or 'patch'") + + @deprecated("Use .bump() instead") + def increase(self, part: str) -> None: + return self.bump(part) + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + +@deprecated("Use Version instead") +class VersionType: + def __init__(self, major: int, minor: int = 0, patch: int = 0) -> None: + self.major = major + self.minor = minor + self.patch = patch + + +concatenate("three", "thirteen") +add(3, 13) +VersionType(3, 13) + +version = Version(3, 13) +version.increase("patch") +print(version) +print(version.bugfix) diff --git a/python-313/typing/generic_queue.py b/python-313/typing/generic_queue.py new file mode 100644 index 0000000000..812c835971 --- /dev/null +++ b/python-313/typing/generic_queue.py @@ -0,0 +1,38 @@ +from collections import deque + + +class Queue[T]: + def __init__(self) -> None: + self.elements: deque[T] = deque() + + def push(self, element: T) -> None: + self.elements.append(element) + + def pop(self) -> T: + return self.elements.popleft() + + +# %% Python 3.13 +# +# class Queue[T=str]: +# def __init__(self) -> None: +# self.elements: deque[T] = deque() +# +# def push(self, element: T) -> None: +# self.elements.append(element) +# +# def pop(self) -> T: +# return self.elements.popleft() + +# %% Use the queue +# +string_queue = Queue() +integer_queue = Queue[int]() + +string_queue.push("three") +string_queue.push("thirteen") +print(string_queue.elements) + +integer_queue.push(3) +integer_queue.push(13) +print(integer_queue.elements) diff --git a/python-313/typing/pyproject.toml b/python-313/typing/pyproject.toml new file mode 100644 index 0000000000..be829bb0a3 --- /dev/null +++ b/python-313/typing/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pyright] +reportDeprecated = true diff --git a/python-313/typing/readonly.py b/python-313/typing/readonly.py new file mode 100644 index 0000000000..08260f0241 --- /dev/null +++ b/python-313/typing/readonly.py @@ -0,0 +1,45 @@ +"""Demonstration of PEP 705: TypedDict: read-only items + +Use PyLance in VS Code by setting Python › Analysis: Type Checking Mode or run +the Pyright CLI: + + $ python -m pip install pyright $ pyright --pythonversion 3.13 . + +Extension of TypedDict: +https://realpython.com/python38-new-features/#more-precise-types +""" + +from typing import NotRequired, ReadOnly, TypedDict + +# class Version(TypedDict): +# version: str +# release_year: NotRequired[int | None] + + +# class PythonVersion(TypedDict): +# version: str +# release_year: int + +class Version(TypedDict): + version: ReadOnly[str] + release_year: ReadOnly[NotRequired[int | None]] + + +class PythonVersion(TypedDict): + version: ReadOnly[str] + release_year: ReadOnly[int] + +py313 = PythonVersion(version="3.13", release_year=2024) + +# Alternative syntax, using TypedDict as an annotation +# py313: PythonVersion = {"version": "3.13", "release_year": 2024} + + +def get_version_info(ver: Version) -> str: + if "release_year" in ver: + return f"Version {ver["version"]} released in {ver["release_year"]}" + else: + return f"Version {ver["version"]}" + +# Only allowed to use PythonVersion instead of Version if the fields are ReadOnly +print(get_version_info(py313)) diff --git a/python-313/typing/tree.py b/python-313/typing/tree.py new file mode 100644 index 0000000000..f0fb417dd4 --- /dev/null +++ b/python-313/typing/tree.py @@ -0,0 +1,34 @@ +from typing import TypeGuard + +type Tree = list[Tree | int] + + +def is_tree(obj: object) -> TypeGuard[Tree]: + return isinstance(obj, list) + + +def get_left_leaf_value(tree_or_leaf: Tree | int) -> int: + if is_tree(tree_or_leaf): + return get_left_leaf_value(tree_or_leaf[0]) + else: + return tree_or_leaf + + +# %% Python 3.13 +# +# from typing import TypeIs +# +# type Tree = list[Tree | int] +# +# def is_tree(obj: object) -> TypeIs[Tree]: +# return isinstance(obj, list) +# +# def get_left_leaf_value(tree_or_leaf: Tree | int) -> int: +# if is_tree(tree_or_leaf): +# return get_left_leaf_value(tree_or_leaf[0]) +# else: +# return tree_or_leaf + +# %% Use the tree +# +print(get_left_leaf_value([[[[3, 13], 12], 11], 10])) From 886bdfb7277f572903a05deabd9dccd95c650797 Mon Sep 17 00:00:00 2001 From: gahjelle Date: Thu, 26 Sep 2024 14:25:25 +0200 Subject: [PATCH 25/29] Fix 3.12 style f-string quotes --- python-313/typing/readonly.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python-313/typing/readonly.py b/python-313/typing/readonly.py index 08260f0241..5e4e886f1f 100644 --- a/python-313/typing/readonly.py +++ b/python-313/typing/readonly.py @@ -20,6 +20,7 @@ # version: str # release_year: int + class Version(TypedDict): version: ReadOnly[str] release_year: ReadOnly[NotRequired[int | None]] @@ -29,6 +30,7 @@ class PythonVersion(TypedDict): version: ReadOnly[str] release_year: ReadOnly[int] + py313 = PythonVersion(version="3.13", release_year=2024) # Alternative syntax, using TypedDict as an annotation @@ -37,9 +39,10 @@ class PythonVersion(TypedDict): def get_version_info(ver: Version) -> str: if "release_year" in ver: - return f"Version {ver["version"]} released in {ver["release_year"]}" + return f"Version {ver['version']} released in {ver['release_year']}" else: - return f"Version {ver["version"]}" + return f"Version {ver['version']}" + # Only allowed to use PythonVersion instead of Version if the fields are ReadOnly print(get_version_info(py313)) From c243ce0713652f22f2170d8e55f93085c336115a Mon Sep 17 00:00:00 2001 From: gahjelle Date: Fri, 27 Sep 2024 10:22:30 +0200 Subject: [PATCH 26/29] TR updates --- python-313/typing/deprecations.py | 9 ++++++--- python-313/typing/readonly.py | 26 +++++++++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/python-313/typing/deprecations.py b/python-313/typing/deprecations.py index 9f8c3c2e4c..7fe8a8fa99 100644 --- a/python-313/typing/deprecations.py +++ b/python-313/typing/deprecations.py @@ -1,12 +1,15 @@ """Demonstration of PEP 702: Marking deprecations using the type system +The deprecations should be marked in PyCharm and VS Code. + Use PyLance in VS Code by setting Python › Analysis: Type Checking Mode or run the Pyright CLI: - $ python -m pip install pyright $ pyright --pythonversion 3.13 . + $ python -m pip install pyright + $ pyright --pythonversion 3.13 . -Note that showing warnings requires setting the reportDeprecated option in -Pyright. This is done in pyproject.toml. +Note that showing warnings with Pyright requires setting the reportDeprecated +option. This is done in the accompanying pyproject.toml. """ from typing import overload diff --git a/python-313/typing/readonly.py b/python-313/typing/readonly.py index 5e4e886f1f..05ed0f92f2 100644 --- a/python-313/typing/readonly.py +++ b/python-313/typing/readonly.py @@ -11,6 +11,8 @@ from typing import NotRequired, ReadOnly, TypedDict +# %% Without ReadOnly + # class Version(TypedDict): # version: str # release_year: NotRequired[int | None] @@ -21,22 +23,24 @@ # release_year: int +# %% Using ReadOnly +# +# Can only use PythonVersion as a Version if the differing fields are ReadOnly class Version(TypedDict): - version: ReadOnly[str] + version: str release_year: ReadOnly[NotRequired[int | None]] + # Note that ReadOnly can be nested with other special forms in any order + # release_year: NotRequired[ReadOnly[int | None]] + class PythonVersion(TypedDict): - version: ReadOnly[str] + version: str release_year: ReadOnly[int] -py313 = PythonVersion(version="3.13", release_year=2024) - -# Alternative syntax, using TypedDict as an annotation -# py313: PythonVersion = {"version": "3.13", "release_year": 2024} - - +# %% Work with Version and PythonVersion +# def get_version_info(ver: Version) -> str: if "release_year" in ver: return f"Version {ver['version']} released in {ver['release_year']}" @@ -44,5 +48,9 @@ def get_version_info(ver: Version) -> str: return f"Version {ver['version']}" -# Only allowed to use PythonVersion instead of Version if the fields are ReadOnly +py313 = PythonVersion(version="3.13", release_year=2024) + +# Alternative syntax, using TypedDict as an annotation +# py313: PythonVersion = {"version": "3.13", "release_year": 2024} + print(get_version_info(py313)) From 16be0cfffcf989bba67f0cf4842b48e842d39967 Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Sat, 28 Sep 2024 12:16:53 +0200 Subject: [PATCH 27/29] Final QA (#588) --- python-313/replace.py | 35 +++++++++++++++++++++++++++++++++++ python-313/typing/tree.py | 8 ++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/python-313/replace.py b/python-313/replace.py index 00b994bcc1..c392e1a766 100644 --- a/python-313/replace.py +++ b/python-313/replace.py @@ -21,3 +21,38 @@ class Person(NamedTuple): person = Person(name="Geir Arne", place="Oslo", version="3.12") print(copy.replace(person, version="3.13")) print(copy.replace(today, day=1)) + + +# %% Create a custom class that supports copy.replace() +class NamedContainer: + def __init__(self, name, **items): + print(f"Initializing {name} with {items}") + self.name = name + self.items = items + + def __replace__(self, **kwargs): + """.__replace__() is called by copy.replace()""" + if "name" in kwargs: + raise ValueError("'name' can't be updated") + + print(f"Replacing {kwargs} in {self.name}") + init_kwargs = {"name": self.name} | self.items | kwargs + + # Create a new object with updated arguments + cls = type(self) + return cls(**init_kwargs) + + def __repr__(self): + items = [f"{key}={value!r}" for key, value in self.items.items()] + return f"{type(self).__name__}(name='{self.name}', {", ".join(items)})" + + +capitals = NamedContainer( + "capitals", norway="oslo", sweden="Stockholm", denmark="Copenhagen" +) +print(f"{capitals = }") + +capitals = copy.replace(capitals, norway="Oslo") +print(f"{capitals = }") + +# copy.replace(capitals, name="Scandinavia") # Raises an error, name can't be replaced diff --git a/python-313/typing/tree.py b/python-313/typing/tree.py index f0fb417dd4..bc7471dbe5 100644 --- a/python-313/typing/tree.py +++ b/python-313/typing/tree.py @@ -4,7 +4,9 @@ def is_tree(obj: object) -> TypeGuard[Tree]: - return isinstance(obj, list) + return isinstance(obj, list) and all( + is_tree(elem) or isinstance(elem, int) for elem in obj + ) def get_left_leaf_value(tree_or_leaf: Tree | int) -> int: @@ -21,7 +23,9 @@ def get_left_leaf_value(tree_or_leaf: Tree | int) -> int: # type Tree = list[Tree | int] # # def is_tree(obj: object) -> TypeIs[Tree]: -# return isinstance(obj, list) +# return isinstance(obj, list) and all( +# is_tree(elem) or isinstance(elem, int) for elem in obj +# ) # # def get_left_leaf_value(tree_or_leaf: Tree | int) -> int: # if is_tree(tree_or_leaf): From cad104839a9a0e4bab0530fb635437c98a4fcfb8 Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Fri, 4 Oct 2024 18:08:40 +0200 Subject: [PATCH 28/29] Final QA (#592) --- python-property/point_v6.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python-property/point_v6.py b/python-property/point_v6.py index c974021d6d..2f3be5ddaf 100644 --- a/python-property/point_v6.py +++ b/python-property/point_v6.py @@ -8,11 +8,11 @@ def __init__(self, x, y): @property def distance(self): - return round(math.dist((0, 0), (self.x, self.y))) + return math.dist((0, 0), (self.x, self.y)) @property def angle(self): - return round(math.degrees(math.atan(self.y / self.x)), 1) + return math.degrees(math.atan2(self.y, self.x)) def as_cartesian(self): return self.x, self.y From d5abe4047a1a647baf048ed64248105b11867e65 Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Tue, 8 Oct 2024 22:34:24 +0200 Subject: [PATCH 29/29] Final QA (#594) --- python-closure/roots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python-closure/roots.py b/python-closure/roots.py index 57275bc3da..970664f29e 100644 --- a/python-closure/roots.py +++ b/python-closure/roots.py @@ -8,7 +8,7 @@ def root_calculator(number): square_root = make_root_calculator(2, 4) print(square_root(42)) -cubic_root = make_root_calculator(3, 2) +cubic_root = make_root_calculator(3) print(cubic_root(42)) @@ -24,5 +24,5 @@ def __call__(self, number): square_root = RootCalculator(2, 4) print(square_root(42)) -cubic_root = RootCalculator(3, 2) +cubic_root = RootCalculator(3) print(cubic_root(42))