From e06ae430d1f80e26eb7d1d14d5bba42eaac61c38 Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Mon, 25 Sep 2023 08:36:11 +0200 Subject: [PATCH] Add 3.12 examples from the typing preview (#438) * Add 3.12 examples from the typing preview * Use Python 3.12 for linting * Upgrade Flake8 * Downgrade back to Python 3.11 for linting * Reset cache * Combine 3.11 and 3.12 code in the same file * Update README * README LE Static Typing --------- Co-authored-by: KateFinegan <95366190+KateFinegan@users.noreply.github.com> --- .github/workflows/linters.yml | 6 +-- python-312/README.md | 35 +++++++++++++- python-312/typing/alias.py | 14 ++++++ python-312/typing/concatenation.py | 13 ++++++ python-312/typing/deck.py | 18 ++++++++ python-312/typing/generic_queue.py | 31 +++++++++++++ python-312/typing/inspect_string.py | 25 ++++++++++ python-312/typing/list_helpers.py | 15 ++++++ python-312/typing/options.py | 34 ++++++++++++++ python-312/typing/oslo.question | 5 ++ python-312/typing/pyproject.toml | 2 + python-312/typing/quiz.py | 72 +++++++++++++++++++++++++++++ python-312/typing/typed_queue.py | 23 +++++++++ requirements.txt | 2 +- 14 files changed, 290 insertions(+), 5 deletions(-) create mode 100644 python-312/typing/alias.py create mode 100644 python-312/typing/concatenation.py create mode 100644 python-312/typing/deck.py create mode 100644 python-312/typing/generic_queue.py create mode 100644 python-312/typing/inspect_string.py create mode 100644 python-312/typing/list_helpers.py create mode 100644 python-312/typing/options.py create mode 100644 python-312/typing/oslo.question create mode 100644 python-312/typing/pyproject.toml create mode 100644 python-312/typing/quiz.py create mode 100644 python-312/typing/typed_queue.py diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index b51144d493..2983f1f14c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: include: - - {name: Linux311, python: '3.11.0-rc.1', os: ubuntu-latest} + - {name: Linux311, python: '3.11.5', os: ubuntu-latest} steps: - name: Check out repository uses: actions/checkout@v2 @@ -33,9 +33,9 @@ jobs: uses: actions/cache@v3 with: path: ./venv - key: ${{ matrix.name }}-pip-${{ hashFiles('requirements.txt') }} + key: ${{ matrix.name }}-v1-pip-${{ hashFiles('requirements.txt') }} restore-keys: | - ${{ matrix.name }}-pip- + ${{ matrix.name }}-v1-pip- - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' diff --git a/python-312/README.md b/python-312/README.md index 3e63d19905..ea8679a4be 100644 --- a/python-312/README.md +++ b/python-312/README.md @@ -15,8 +15,10 @@ You can learn more about Python 3.12's new features in the following Real Python - [Python 3.12 Preview: Ever Better Error Messages](https://realpython.com/python312-error-messages/) - [Python 3.12 Preview: Support For the Linux `perf` Profiler](https://realpython.com/python312-perf-profiler/) - [Python 3.12 Preview: More Intuitive and Consistent F-Strings](https://realpython.com/python312-f-strings/) +- [Python 3.12 Preview: Subinterpreters](https://realpython.com/python312-subinterpreters/) +- [Python 3.12 Preview: Static Typing Improvements](https://realpython.com/python312-typing/) -You'll find examples from all these tutorials in this repository. +You'll find examples from these tutorials in this repository. ## Examples @@ -136,6 +138,37 @@ Pythonista! In this example, you can see how the new implementation of f-strings allows you to include backslashes in embedded expressions. This wasn't possible with f-strings in earlier versions of Python. +### Static Typing Improvements + +You'll find all static typing examples inside the [`typing/`](typing/) directory. You should install the Pyright type checker from PyPI: + +```console +$ python -m pip install pyright +``` + +You can then run type checks by running `pyright`. For some features, you need to specify `--pythonversion 3.12`. + +#### Type Variables and Generic Classes, Functions, and Type Aliases + +You can find comparisons between the old and the new syntax for type variables in the following files, with the new 3.12 syntax shown in the commented part of the code: + +- [`generic_queue.py`](typing/generic_queue.py) +- [`list_helpers.py`](typing/list_helpers.py) +- [`concatenation.py`](typing/concatenation.py) +- [`inspect_string.py`](typing/inspect_string.py) +- [`deck.py`](typing/deck.py) +- [`alias.py`](typing/alias.py) + +Additionally, [`typed_queue.py`](typing/typed_queue.py) shows the implementation of typed queues without using type variables. + +#### Modeling Inheritance With `@override` + +The file [`quiz.py`](typing/quiz.py) shows how to use the new `@override` decorator. In addition to the code in the tutorial, this file includes support for reading questions from files. This is done to show that `@override` works well together with other decorators like `@classmethod`. + +#### Annotating `**kwargs` With Typed Dictionaries + +The file [`options.py`](typing/options.py) shows how you can use a typed dictionary to annotate variable keyword arguments. + ## Authors - **Martin Breuss**, E-mail: [martin@realpython.com](martin@realpython.com) diff --git a/python-312/typing/alias.py b/python-312/typing/alias.py new file mode 100644 index 0000000000..0b203b37e2 --- /dev/null +++ b/python-312/typing/alias.py @@ -0,0 +1,14 @@ +from typing import TypeAlias, TypeVar + +T = TypeVar("T") + +Ordered: TypeAlias = list[T] | tuple[T, ...] + +numbers: Ordered[int] = (1, 2, 3) + + +# %% Python 3.12 + +# type Ordered[T] = list[T] | tuple[T, ...] +# +# numbers: Ordered[int] = (1, 2, 3) diff --git a/python-312/typing/concatenation.py b/python-312/typing/concatenation.py new file mode 100644 index 0000000000..fe429f6433 --- /dev/null +++ b/python-312/typing/concatenation.py @@ -0,0 +1,13 @@ +from typing import TypeVar + +T = TypeVar("T", str, bytes) + + +def concatenate(first: T, second: T) -> T: + return first + second + + +# %% Python 3.12 + +# def concatenate[T: (str, bytes)](first: T, second: T) -> T: +# return first + second diff --git a/python-312/typing/deck.py b/python-312/typing/deck.py new file mode 100644 index 0000000000..1b4644a547 --- /dev/null +++ b/python-312/typing/deck.py @@ -0,0 +1,18 @@ +import random +from typing import TypeAlias + +CardDeck: TypeAlias = list[tuple[str, int]] + + +def shuffle(deck: CardDeck) -> CardDeck: + return random.sample(deck, k=len(deck)) + + +# %% Python 3.12 + +# import random +# +# type CardDeck = list[tuple[str, int]] +# +# def shuffle(deck: CardDeck) -> CardDeck: +# return random.sample(deck, k=len(deck)) diff --git a/python-312/typing/generic_queue.py b/python-312/typing/generic_queue.py new file mode 100644 index 0000000000..3ebbe6b635 --- /dev/null +++ b/python-312/typing/generic_queue.py @@ -0,0 +1,31 @@ +from collections import deque +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class Queue(Generic[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.12 + +# 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() diff --git a/python-312/typing/inspect_string.py b/python-312/typing/inspect_string.py new file mode 100644 index 0000000000..f7767d6269 --- /dev/null +++ b/python-312/typing/inspect_string.py @@ -0,0 +1,25 @@ +from typing import TypeVar + +S = TypeVar("S", bound=str) + + +class Words(str): + def __len__(self): + return len(self.split()) + + +def inspect(text: S) -> S: + print(f"'{text.upper()}' has length {len(text)}") + return text + + +# %% Python 3.12 + +# class Words(str): +# def __len__(self): +# return len(self.split()) +# +# +# def inspect[S: str](text: S) -> S: +# print(f"'{text.upper()}' has length {len(text)}") +# return text diff --git a/python-312/typing/list_helpers.py b/python-312/typing/list_helpers.py new file mode 100644 index 0000000000..61a399b2ec --- /dev/null +++ b/python-312/typing/list_helpers.py @@ -0,0 +1,15 @@ +from typing import TypeVar + +T = TypeVar("T") + + +def push_and_pop(elements: list[T], element: T) -> T: + elements.append(element) + return elements.pop(0) + + +# %% Python 3.12 + +# def push_and_pop[T](elements: list[T], element: T) -> T: +# elements.append(element) +# return elements.pop(0) diff --git a/python-312/typing/options.py b/python-312/typing/options.py new file mode 100644 index 0000000000..9086adfd6c --- /dev/null +++ b/python-312/typing/options.py @@ -0,0 +1,34 @@ +from typing import Required, TypedDict, Unpack + + +class Options(TypedDict, total=False): + line_width: int + level: Required[str] + propagate: bool + + +def show_options(program_name: str, **kwargs: Unpack[Options]) -> None: + print(program_name.upper()) + for option, value in kwargs.items(): + print(f"{option:<15} {value}") + + +def show_options_explicit( + program_name: str, + *, + level: str, + line_width: int | None = None, + propagate: bool | None = None, +) -> None: + options = { + "line_width": line_width, + "level": level, + "propagate": propagate, + } + print(program_name.upper()) + for option, value in options.items(): + if value is not None: + print(f"{option:<15} {value}") + + +show_options("logger", line_width=80, level="INFO", propagate=False) diff --git a/python-312/typing/oslo.question b/python-312/typing/oslo.question new file mode 100644 index 0000000000..a021f6f7df --- /dev/null +++ b/python-312/typing/oslo.question @@ -0,0 +1,5 @@ +In which country is Oslo the capital? +Norway +Sweden +Ireland +Canada diff --git a/python-312/typing/pyproject.toml b/python-312/typing/pyproject.toml new file mode 100644 index 0000000000..d8321d405f --- /dev/null +++ b/python-312/typing/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pyright] +# reportImplicitOverride = true diff --git a/python-312/typing/quiz.py b/python-312/typing/quiz.py new file mode 100644 index 0000000000..8719033c2a --- /dev/null +++ b/python-312/typing/quiz.py @@ -0,0 +1,72 @@ +import pathlib +import random +from dataclasses import dataclass +from string import ascii_lowercase +from typing import Self, override + + +@dataclass +class Question: + question: str + answer: str + + @classmethod + def from_file(cls, path: pathlib.Path) -> Self: + question, answer, *_ = path.read_text(encoding="utf-8").split("\n") + return cls(question, answer) + + def ask(self) -> bool: + answer = input(f"\n{self.question} ") + return answer == self.answer + + +@dataclass +class MultipleChoiceQuestion(Question): + distractors: list[str] + + @classmethod + @override + def from_file(cls, path: pathlib.Path) -> Self: + question, answer, *distractors = ( + path.read_text(encoding="utf-8").strip().split("\n") + ) + return cls(question, answer, distractors) + + @override + def ask(self) -> bool: + print(f"\n{self.question}") + + alternatives = random.sample( + self.distractors + [self.answer], k=len(self.distractors) + 1 + ) + labeled_alternatives = dict(zip(ascii_lowercase, alternatives)) + for label, alternative in labeled_alternatives.items(): + print(f" {label}) {alternative}", end="") + + answer = input("\n\nChoice? ") + return labeled_alternatives.get(answer) == self.answer + + +questions = [ + Question("Who created Python?", "Guido van Rossum"), + MultipleChoiceQuestion( + "What's a PEP?", + "A Python Enhancement Proposal", + distractors=[ + "A Pretty Exciting Policy", + "A Preciously Evolved Python", + "A Potentially Epic Prize", + ], + ), + MultipleChoiceQuestion.from_file(pathlib.Path("oslo.question")), +] + +score = 0 +for question in random.sample(questions, k=len(questions)): + if question.ask(): + score += 1 + print("Yes, that's correct!") + else: + print(f"No, the answer is '{question.answer}'") + +print(f"\nYou got {score} out of {len(questions)} correct") diff --git a/python-312/typing/typed_queue.py b/python-312/typing/typed_queue.py new file mode 100644 index 0000000000..99fede6982 --- /dev/null +++ b/python-312/typing/typed_queue.py @@ -0,0 +1,23 @@ +from collections import deque + + +class IntegerQueue: + def __init__(self) -> None: + self.elements: deque[int] = deque() + + def push(self, element: int) -> None: + self.elements.append(element) + + def pop(self) -> int: + return self.elements.popleft() + + +class StringQueue: + def __init__(self) -> None: + self.elements: deque[str] = deque() + + def push(self, element: str) -> None: + self.elements.append(element) + + def pop(self) -> str: + return self.elements.popleft() diff --git a/requirements.txt b/requirements.txt index ceddbc5f57..ed52968a38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ black[jupyter]==22.6.0 -flake8==5.0.4 +flake8==6.1.0