Skip to content

Remove support for Python 3.9 #4827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14.0-beta.3']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14.0-beta.3']
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: False
steps:
Expand Down Expand Up @@ -59,9 +59,15 @@ jobs:
pytest -v --cov -k "${TO_TEST}" --junit-xml=.test_report_no_optionals_junit.xml
opt_dep_status=$?

<<<<<<< HEAD
pip install .[all]
# Test the rest
export TEST_WITH_OPT_DEPS='true'
=======
# Test the rest
export TEST_WITH_OPT_DEPS='true'
pip install .[all]
>>>>>>> master
# `-n auto --dist worksteal` uses pytest-xdist to run tests on multiple CPU
# workers. Increasing number of workers has little effect on test duration, but it seems
# to increase flakyness.
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ repos:
hooks:
- id: pyupgrade
args:
- --py39-plus
- --py310-plus
- repo: https://github.com/pycqa/isort
rev: 6.0.1
hooks:
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Introduction

This library provides a pure Python, asynchronous interface for the
`Telegram Bot API <https://core.telegram.org/bots/api>`_.
It's compatible with Python versions **3.9+**.
It's compatible with Python versions **3.10+**.

In addition to the pure API implementation, this library features several convenience methods and shortcuts as well as a number of high-level classes to
make the development of bots easy and straightforward. These classes are contained in the
Expand Down
3 changes: 1 addition & 2 deletions changes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import re
from collections.abc import Collection
from pathlib import Path
from typing import Optional

from chango import Version
from chango.concrete import DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory
Expand Down Expand Up @@ -39,7 +38,7 @@ class ChangoSectionChangeNote(
def get_sections(
cls,
labels: Collection[str],
issue_types: Optional[Collection[str]],
issue_types: Collection[str] | None,
) -> set[str]:
"""Override get_sections to have customized auto-detection of relevant sections based on
the pull request and linked issues. Certainly not perfect in all cases, but should be a
Expand Down
5 changes: 5 additions & 0 deletions changes/unreleased/4825.R7wiTzvN37KAV656s9kfnC.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
other = "Add support for Python 3.14 beta"
[[pull_requests]]
uid = "4825"
author_uid = "harshil21"
closes_threads = []
5 changes: 5 additions & 0 deletions changes/unreleased/4827.PBXyEEjvXz5sJbiDWkeGFX.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
other = "Remove support for Python 3.9"
[[pull_requests]]
uid = "4827"
author_uid = "harshil21"
closes_threads = []
14 changes: 9 additions & 5 deletions docs/auxil/admonition_inserter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
import contextlib
import inspect
import re
import types
import typing
from collections import defaultdict
from collections.abc import Iterator
from socket import socket
from types import FunctionType
from typing import Union

from apscheduler.job import Job as APSJob

Expand Down Expand Up @@ -108,7 +108,7 @@ class AdmonitionInserter:
"""

def __init__(self):
self.admonitions: dict[str, dict[Union[type, collections.abc.Callable], str]] = {
self.admonitions: dict[str, dict[type | collections.abc.Callable, str]] = {
# dynamically determine which method to use to create a sub-dictionary
admonition_type: getattr(self, f"_create_{admonition_type}")()
for admonition_type in self.ALL_ADMONITION_TYPES
Expand Down Expand Up @@ -136,7 +136,7 @@ def __init__(self):

def insert_admonitions(
self,
obj: Union[type, collections.abc.Callable],
obj: type | collections.abc.Callable,
docstring_lines: list[str],
):
"""Inserts admonitions into docstring lines for a given class or method.
Expand Down Expand Up @@ -324,6 +324,8 @@ def _create_use_in(self) -> dict[type, str]:

for cls, method_names in self.METHOD_NAMES_FOR_BOT_APP_APPBUILDER.items():
for method_name in method_names:
if method_name == "get_file":
pass
method_link = self._generate_link_to_method(method_name, cls)

arg = getattr(cls, method_name)
Expand Down Expand Up @@ -509,7 +511,9 @@ def _is_ptb_class(cls: type) -> bool:
def recurse_type(type_, is_recursed_from_ptb_class: bool):
next_is_recursed_from_ptb_class = is_recursed_from_ptb_class or _is_ptb_class(type_)

if hasattr(type_, "__origin__"): # For generic types like Union, List, etc.
if hasattr(type_, "__origin__") or isinstance(
type_, types.UnionType
): # For generic types like Union, List, etc.
# Make sure it's not a telegram.ext generic type (e.g. ContextTypes[...])
org = typing.get_origin(type_)
if "telegram.ext" in str(org):
Expand Down Expand Up @@ -541,7 +545,7 @@ def recurse_type(type_, is_recursed_from_ptb_class: bool):
return list(telegram_classes)

@staticmethod
def _resolve_class(name: str) -> Union[type, None]:
def _resolve_class(name: str) -> type | None:
"""The keys in the admonitions dictionary are not strings like "telegram.StickerSet"
but classes like <class 'telegram._files.sticker.StickerSet'>.

Expand Down
3 changes: 1 addition & 2 deletions examples/chatmemberbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"""

import logging
from typing import Optional

from telegram import Chat, ChatMember, ChatMemberUpdated, Update
from telegram.constants import ParseMode
Expand All @@ -37,7 +36,7 @@
logger = logging.getLogger(__name__)


def extract_status_change(chat_member_update: ChatMemberUpdated) -> Optional[tuple[bool, bool]]:
def extract_status_change(chat_member_update: ChatMemberUpdated) -> tuple[bool, bool] | None:
"""Takes a ChatMemberUpdated instance and extracts whether the 'old_chat_member' was a member
of the chat and whether the 'new_chat_member' is a member of the chat. Returns None, if
the status didn't change.
Expand Down
9 changes: 4 additions & 5 deletions examples/contexttypesbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import logging
from collections import defaultdict
from typing import Optional

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ParseMode
Expand Down Expand Up @@ -50,19 +49,19 @@ class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
def __init__(
self,
application: Application,
chat_id: Optional[int] = None,
user_id: Optional[int] = None,
chat_id: int | None = None,
user_id: int | None = None,
):
super().__init__(application=application, chat_id=chat_id, user_id=user_id)
self._message_id: Optional[int] = None
self._message_id: int | None = None

@property
def bot_user_ids(self) -> set[int]:
"""Custom shortcut to access a value stored in the bot_data dict"""
return self.bot_data.setdefault("user_ids", set())

@property
def message_clicks(self) -> Optional[int]:
def message_clicks(self) -> int | None:
"""Access the number of clicks for the message this context object was built for."""
if self._message_id:
return self.chat_data.clicks_per_message[self._message_id]
Expand Down
11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dynamic = ["version"]
name = "python-telegram-bot"
description = "We have made you a wrapper you can't refuse"
readme = "README.rst"
requires-python = ">=3.9"
requires-python = ">=3.10"
license = "LGPL-3.0-only"
license-files = ["LICENSE", "LICENSE.dual", "LICENSE.lesser"]
authors = [
Expand All @@ -31,7 +31,6 @@ classifiers = [
"Topic :: Internet",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -40,6 +39,7 @@ classifiers = [
]
dependencies = [
"httpx >=0.27,<0.29",
"httpcore >=1.0.9; python_version >= '3.14'" # httpx needs an update so 3.14 support works
]

[project.urls]
Expand Down Expand Up @@ -205,6 +205,7 @@ disable = ["duplicate-code", "too-many-arguments", "too-many-public-methods",
# run pylint across multiple cpu cores to speed it up-
# https://pylint.pycqa.org/en/latest/user_guide/run.html?#parallel-execution to know more
jobs = 0
py-version = "3.10"

[tool.pylint.classes]
exclude-protected = ["_unfrozen"]
Expand Down Expand Up @@ -242,7 +243,7 @@ disallow_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_decorators = true
show_error_codes = true
python_version = "3.9"
python_version = "3.10"

# For some files, it's easier to just disable strict-optional all together instead of
# cluttering the code with `# type: ignore`s or stuff like
Expand Down Expand Up @@ -285,6 +286,10 @@ omit = [
"tests/",
"src/telegram/__main__.py"
]
# Relevant for python 3.14: https://github.com/nedbat/coveragepy/issues/1983
disable_warnings = [
"no-ctracer",
]

[tool.coverage.report]
exclude_also = [
Expand Down
3 changes: 1 addition & 2 deletions src/telegram/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@
# ruff: noqa: T201, D100, S603, S607
import subprocess
import sys
from typing import Optional

from . import __version__ as telegram_ver
from .constants import BOT_API_VERSION


def _git_revision() -> Optional[str]:
def _git_revision() -> str | None:
try:
output = subprocess.check_output(
["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT
Expand Down
9 changes: 4 additions & 5 deletions src/telegram/_birthdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Birthday."""
import datetime as dtm
from typing import Optional

from telegram._telegramobject import TelegramObject
from telegram._utils.types import JSONDict
Expand Down Expand Up @@ -51,17 +50,17 @@ def __init__(
self,
day: int,
month: int,
year: Optional[int] = None,
year: int | None = None,
*,
api_kwargs: Optional[JSONDict] = None,
api_kwargs: JSONDict | None = None,
):
super().__init__(api_kwargs=api_kwargs)

# Required
self.day: int = day
self.month: int = month
# Optional
self.year: Optional[int] = year
self.year: int | None = year

self._id_attrs = (
self.day,
Expand All @@ -70,7 +69,7 @@ def __init__(

self._freeze()

def to_date(self, year: Optional[int] = None) -> dtm.date:
def to_date(self, year: int | None = None) -> dtm.date:
"""Return the birthdate as a date object.

.. versionchanged:: 21.2
Expand Down
Loading