diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b34fe4f7..988cf67dc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,10 +24,6 @@ jobs: - '3.13' - pypy-3.10 include: - - os: ubuntu-20.04 - python-version: '3.5' - - os: ubuntu-20.04 - python-version: '3.6' - os: windows-2022 python-version: '3.12' - os: macos-13 diff --git a/loguru/__init__.pyi b/loguru/__init__.pyi index 101629148..8d3f8855d 100644 --- a/loguru/__init__.pyi +++ b/loguru/__init__.pyi @@ -92,11 +92,14 @@ from asyncio import AbstractEventLoop from datetime import datetime, time, timedelta from logging import Handler from multiprocessing.context import BaseContext +from os import PathLike from types import TracebackType from typing import ( Any, + Awaitable, BinaryIO, Callable, + ContextManager, Dict, Generator, Generic, @@ -114,26 +117,13 @@ from typing import ( overload, ) -if sys.version_info >= (3, 5, 3): - from typing import Awaitable -else: - from typing_extensions import Awaitable - -if sys.version_info >= (3, 6): - from os import PathLike - from typing import ContextManager - - PathLikeStr = PathLike[str] -else: - from pathlib import PurePath as PathLikeStr - - from typing_extensions import ContextManager - if sys.version_info >= (3, 8): from typing import Protocol, TypedDict else: from typing_extensions import Protocol, TypedDict +PathLikeStr = PathLike[str] + _T = TypeVar("_T") _F = TypeVar("_F", bound=Callable[..., Any]) ExcInfo = Tuple[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]] @@ -248,7 +238,7 @@ class Logger: diagnose: bool = ..., enqueue: bool = ..., context: Optional[Union[str, BaseContext]] = ..., - catch: bool = ... + catch: bool = ..., ) -> int: ... @overload def add( @@ -265,7 +255,7 @@ class Logger: enqueue: bool = ..., context: Optional[Union[str, BaseContext]] = ..., catch: bool = ..., - loop: Optional[AbstractEventLoop] = ... + loop: Optional[AbstractEventLoop] = ..., ) -> int: ... @overload def add( @@ -290,7 +280,7 @@ class Logger: mode: str = ..., buffering: int = ..., encoding: str = ..., - **kwargs: Any + **kwargs: Any, ) -> int: ... def remove(self, handler_id: Optional[int] = ...) -> None: ... def complete(self) -> AwaitableCompleter: ... @@ -304,7 +294,7 @@ class Logger: onerror: Optional[Callable[[BaseException], None]] = ..., exclude: Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]] = ..., default: Any = ..., - message: str = ... + message: str = ..., ) -> Catcher: ... @overload def catch(self, function: _F) -> _F: ... @@ -318,10 +308,10 @@ class Logger: raw: bool = ..., capture: bool = ..., depth: int = ..., - ansi: bool = ... + ansi: bool = ..., ) -> Logger: ... - def bind(__self, **kwargs: Any) -> Logger: ... # noqa: N805 - def contextualize(__self, **kwargs: Any) -> Contextualizer: ... # noqa: N805 + def bind(self, **kwargs: Any) -> Logger: ... # noqa: N805 + def contextualize(self, **kwargs: Any) -> Contextualizer: ... # noqa: N805 def patch(self, patcher: PatcherFunction) -> Logger: ... @overload def level(self, name: str) -> Level: ... @@ -346,7 +336,7 @@ class Logger: levels: Optional[Sequence[LevelConfig]] = ..., extra: Optional[Dict[Any, Any]] = ..., patcher: Optional[PatcherFunction] = ..., - activation: Optional[Sequence[ActivationConfig]] = ... + activation: Optional[Sequence[ActivationConfig]] = ..., ) -> List[int]: ... # @staticmethod cannot be used with @overload in mypy (python/mypy#7781). # However Logger is not exposed and logger is an instance of Logger @@ -359,7 +349,7 @@ class Logger: pattern: Union[str, Pattern[str]], *, cast: Union[Dict[str, Callable[[str], Any]], Callable[[Dict[str, str]], None]] = ..., - chunk: int = ... + chunk: int = ..., ) -> Generator[Dict[str, Any], None, None]: ... @overload def parse( @@ -368,7 +358,7 @@ class Logger: pattern: Union[bytes, Pattern[bytes]], *, cast: Union[Dict[str, Callable[[bytes], Any]], Callable[[Dict[str, bytes]], None]] = ..., - chunk: int = ... + chunk: int = ..., ) -> Generator[Dict[str, Any], None, None]: ... @overload def trace(__self, __message: str, *args: Any, **kwargs: Any) -> None: ... # noqa: N805 @@ -404,10 +394,10 @@ class Logger: def exception(__self, __message: Any) -> None: ... # noqa: N805 @overload def log( - __self, __level: Union[int, str], __message: str, *args: Any, **kwargs: Any # noqa: N805 + self, __level: Union[int, str], __message: str, *args: Any, **kwargs: Any # noqa: N805 ) -> None: ... @overload - def log(__self, __level: Union[int, str], __message: Any) -> None: ... # noqa: N805 + def log(self, __level: Union[int, str], __message: Any) -> None: ... # noqa: N805 def start(self, *args: Any, **kwargs: Any) -> int: ... def stop(self, *args: Any, **kwargs: Any) -> None: ... diff --git a/loguru/_asyncio_loop.py b/loguru/_asyncio_loop.py deleted file mode 100644 index e981955c7..000000000 --- a/loguru/_asyncio_loop.py +++ /dev/null @@ -1,27 +0,0 @@ -import asyncio -import sys - - -def load_loop_functions(): - if sys.version_info >= (3, 7): - - def get_task_loop(task): - return task.get_loop() - - get_running_loop = asyncio.get_running_loop - - else: - - def get_task_loop(task): - return task._loop - - def get_running_loop(): - loop = asyncio.get_event_loop() - if not loop.is_running(): - raise RuntimeError("There is no running event loop") - return loop - - return get_task_loop, get_running_loop - - -get_task_loop, get_running_loop = load_loop_functions() diff --git a/loguru/_better_exceptions.py b/loguru/_better_exceptions.py index 94610c713..a4cedb913 100644 --- a/loguru/_better_exceptions.py +++ b/loguru/_better_exceptions.py @@ -10,23 +10,12 @@ import tokenize import traceback -if sys.version_info >= (3, 11): +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup - def is_exception_group(exc): - return isinstance(exc, ExceptionGroup) -else: - try: - from exceptiongroup import ExceptionGroup - except ImportError: - - def is_exception_group(exc): - return False - - else: - - def is_exception_group(exc): - return isinstance(exc, ExceptionGroup) +def is_exception_group(exc): + return isinstance(exc, ExceptionGroup) class SyntaxHighlighter: diff --git a/loguru/_contextvars.py b/loguru/_contextvars.py deleted file mode 100644 index 2e8bbb292..000000000 --- a/loguru/_contextvars.py +++ /dev/null @@ -1,15 +0,0 @@ -import sys - - -def load_contextvar_class(): - if sys.version_info >= (3, 7): - from contextvars import ContextVar - elif sys.version_info >= (3, 5, 3): - from aiocontextvars import ContextVar - else: - from contextvars import ContextVar - - return ContextVar - - -ContextVar = load_contextvar_class() diff --git a/loguru/_file_sink.py b/loguru/_file_sink.py index 4b09b06bb..7225064d2 100644 --- a/loguru/_file_sink.py +++ b/loguru/_file_sink.py @@ -17,12 +17,12 @@ def generate_rename_path(root, ext, creation_time): creation_datetime = datetime.datetime.fromtimestamp(creation_time) date = FileDateFormatter(creation_datetime) - renamed_path = "{}.{}{}".format(root, date, ext) + renamed_path = f"{root}.{date}{ext}" counter = 1 while os.path.exists(renamed_path): counter += 1 - renamed_path = "{}.{}.{}{}".format(root, date, counter, ext) + renamed_path = f"{root}.{date}.{counter}{ext}" return renamed_path @@ -56,7 +56,7 @@ def copy_compress(path_in, path_out, opener, **kwargs): @staticmethod def compression(path_in, ext, compress_function): - path_out = "{}{}".format(path_in, ext) + path_out = f"{path_in}{ext}" if os.path.exists(path_out): creation_time = get_ctime(path_out) @@ -167,7 +167,7 @@ def __init__( mode="a", buffering=1, encoding="utf8", - **kwargs + **kwargs, ): self.encoding = encoding diff --git a/loguru/_handler.py b/loguru/_handler.py index 81a3dca08..48e86cab0 100644 --- a/loguru/_handler.py +++ b/loguru/_handler.py @@ -45,7 +45,7 @@ def __init__( error_interceptor, exception_formatter, id_, - levels_ansi_codes + levels_ansi_codes, ): self._name = name self._sink = sink diff --git a/loguru/_logger.py b/loguru/_logger.py index b76f1af58..3a72b7ef5 100644 --- a/loguru/_logger.py +++ b/loguru/_logger.py @@ -90,17 +90,19 @@ import re import sys import warnings -from collections import namedtuple +from asyncio import get_running_loop +from contextvars import ContextVar from inspect import isclass, iscoroutinefunction, isgeneratorfunction from multiprocessing import current_process, get_context from multiprocessing.context import BaseContext +from os import PathLike from os.path import basename, splitext from threading import current_thread +from typing import NamedTuple -from . import _asyncio_loop, _colorama, _defaults, _filters +from . import _colorama, _defaults, _filters from ._better_exceptions import ExceptionFormatter from ._colorizer import Colorizer -from ._contextvars import ContextVar from ._datetime import aware_now from ._error_interceptor import ErrorInterceptor from ._file_sink import FileSink @@ -110,13 +112,13 @@ from ._recattrs import RecordException, RecordFile, RecordLevel, RecordProcess, RecordThread from ._simple_sinks import AsyncSink, CallableSink, StandardSink, StreamSink -if sys.version_info >= (3, 6): - from os import PathLike -else: - from pathlib import PurePath as PathLike +class Level(NamedTuple): + name: str + no: int + color: str + icon: str -Level = namedtuple("Level", ["name", "no", "color", "icon"]) start_time = aware_now() @@ -249,7 +251,7 @@ def add( enqueue=_defaults.LOGURU_ENQUEUE, context=_defaults.LOGURU_CONTEXT, catch=_defaults.LOGURU_CATCH, - **kwargs + **kwargs, ): r"""Add a handler sending log messages to a sink adequately configured. @@ -840,7 +842,7 @@ def add( # running loop in Python 3.5.2 and earlier versions, see python/asyncio#452. if enqueue and loop is None: try: - loop = _asyncio_loop.get_running_loop() + loop = get_running_loop() except RuntimeError as e: raise ValueError( "An event loop is required to add a coroutine sink with `enqueue=True`, " @@ -1135,7 +1137,7 @@ def catch( default=None, message="An error has been caught in function '{record[function]}', " "process '{record[process].name}' ({record[process].id}), " - "thread '{record[thread].name}' ({record[thread].id}):" + "thread '{record[thread].name}' ({record[thread].id}):", ): """Return a decorator to automatically log possibly caught error in wrapped function. @@ -1292,7 +1294,7 @@ def opt( raw=False, capture=True, depth=0, - ansi=False + ansi=False, ): r"""Parametrize a logging call to slightly change generated log message. diff --git a/loguru/_recattrs.py b/loguru/_recattrs.py index b09426efb..820c0c30d 100644 --- a/loguru/_recattrs.py +++ b/loguru/_recattrs.py @@ -11,7 +11,7 @@ def __init__(self, name, no, icon): self.icon = icon def __repr__(self): - return "(name=%r, no=%r, icon=%r)" % (self.name, self.no, self.icon) + return f"(name={self.name!r}, no={self.no!r}, icon={self.icon!r})" def __format__(self, spec): return self.name.__format__(spec) @@ -25,7 +25,7 @@ def __init__(self, name, path): self.path = path def __repr__(self): - return "(name=%r, path=%r)" % (self.name, self.path) + return f"(name={self.name!r}, path={self.path!r})" def __format__(self, spec): return self.name.__format__(spec) @@ -39,7 +39,7 @@ def __init__(self, id_, name): self.name = name def __repr__(self): - return "(id=%r, name=%r)" % (self.id, self.name) + return f"(id={self.id!r}, name={self.name!r})" def __format__(self, spec): return self.id.__format__(spec) @@ -53,7 +53,7 @@ def __init__(self, id_, name): self.name = name def __repr__(self): - return "(id=%r, name=%r)" % (self.id, self.name) + return f"(id={self.id!r}, name={self.name!r})" def __format__(self, spec): return self.id.__format__(spec) @@ -61,7 +61,9 @@ def __format__(self, spec): class RecordException(namedtuple("RecordException", ("type", "value", "traceback"))): def __repr__(self): - return "(type=%r, value=%r, traceback=%r)" % (self.type, self.value, self.traceback) + return "(type={!r}, value={!r}, traceback={!r})".format( + self.type, self.value, self.traceback + ) def __reduce__(self): # The traceback is not picklable, therefore it needs to be removed. Additionally, there's a diff --git a/loguru/_simple_sinks.py b/loguru/_simple_sinks.py index 658f1ad65..0bce411d5 100644 --- a/loguru/_simple_sinks.py +++ b/loguru/_simple_sinks.py @@ -1,8 +1,7 @@ import inspect import logging import weakref - -from ._asyncio_loop import get_running_loop, get_task_loop +from asyncio import get_running_loop class StreamSink: @@ -97,7 +96,7 @@ def tasks_to_complete(self): async def _complete_task(self, task): loop = get_running_loop() - if get_task_loop(task) is not loop: + if task.get_loop() is not loop: return try: await task diff --git a/pyproject.toml b/pyproject.toml index 0e5500c75..def17b863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,8 +15,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -30,8 +28,8 @@ classifiers = [ ] dependencies = [ "colorama>=0.3.4 ; sys_platform=='win32'", - "aiocontextvars>=0.2.0 ; python_version<'3.7'", - "win32-setctime>=1.0.0 ; sys_platform=='win32'" + "win32-setctime>=1.0.0 ; sys_platform=='win32'", + "exceptiongroup>=1.1.3,<2 ; python_version<'3.11'" ] description = "Python logging made (stupidly) simple" dynamic = ['version'] @@ -39,7 +37,7 @@ keywords = ["loguru", "logging", "logger", "log"] license = {text = "MIT"} name = "loguru" readme = 'README.rst' -requires-python = ">=3.5,<4.0" +requires-python = ">=3.7,<4.0" [project.optional-dependencies] dev = [ @@ -52,17 +50,16 @@ dev = [ "pytest==8.3.2 ; python_version>='3.8'", "pytest-cov==2.12.1 ; python_version<'3.8'", "pytest-cov==5.0.0 ; python_version>='3.8'", - "pytest-mypy-plugins==1.9.3 ; python_version>='3.6' and python_version<'3.8'", + "pytest-mypy-plugins==1.9.3 ; python_version<'3.8'", "pytest-mypy-plugins==3.1.0 ; python_version>='3.8'", # Testing utils. "colorama==0.4.5 ; python_version<'3.8'", "colorama==0.4.6 ; python_version>='3.8'", "freezegun==1.1.0 ; python_version<'3.8'", "freezegun==1.5.0 ; python_version>='3.8'", - "exceptiongroup==1.1.3 ; python_version>='3.7' and python_version<'3.11'", + "exceptiongroup==1.1.3 ; python_version<'3.11'", # Type checking. - "mypy==v0.910 ; python_version<'3.6'", - "mypy==v0.971 ; python_version>='3.6' and python_version<'3.7'", + "mypy==v0.971 ; python_version<'3.7'", "mypy==v1.4.1 ; python_version>='3.7' and python_version<'3.8'", "mypy==v1.11.2 ; python_version>='3.8'", # Docs. @@ -79,7 +76,7 @@ Homepage = "https://github.com/Delgan/loguru" [tool.black] force-exclude = "tests/exceptions/source/modern/*" line-length = 100 -target-version = ["py35"] +target-version = ["py37"] [tool.flit.module] name = "loguru" diff --git a/tests/conftest.py b/tests/conftest.py index 1f044177f..33b7e12c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ import io import logging import os -import pathlib import sys import threading import time @@ -18,34 +17,6 @@ import loguru -if sys.version_info < (3, 5, 3): - - def run(coro): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - res = loop.run_until_complete(coro) - loop.close() - asyncio.set_event_loop(None) - return res - - asyncio.run = run -elif sys.version_info < (3, 7): - - def run(coro): - loop = asyncio.new_event_loop() - res = loop.run_until_complete(coro) - loop.close() - asyncio.set_event_loop(None) - return res - - asyncio.run = run - -if sys.version_info < (3, 6): - - @pytest.fixture - def tmp_path(tmp_path): - yield pathlib.Path(str(tmp_path)) - @contextlib.contextmanager def new_event_loop_context(): diff --git a/tests/test_contextualize.py b/tests/test_contextualize.py index e1a8fe36d..74546d867 100644 --- a/tests/test_contextualize.py +++ b/tests/test_contextualize.py @@ -1,12 +1,10 @@ import asyncio import sys import threading -from unittest.mock import MagicMock import pytest from loguru import logger -from loguru._contextvars import load_contextvar_class def test_contextualize(writer): @@ -209,13 +207,3 @@ def test_context_reset_despite_error(writer): logger.info("Error") assert writer.read() == "Division {'foobar': 456}\nError {}\n" - - -# There is not CI runner available for Python 3.5.2. Consequently, we are just -# verifying third-library is properly imported to reach 100% coverage. -def test_contextvars_fallback_352(monkeypatch): - mock_module = MagicMock() - with monkeypatch.context() as context: - context.setattr(sys, "version_info", (3, 5, 2)) - context.setitem(sys.modules, "contextvars", mock_module) - assert load_contextvar_class() == mock_module.ContextVar diff --git a/tests/test_datetime.py b/tests/test_datetime.py index ff7f6f905..191639443 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -7,10 +7,7 @@ from loguru import logger -if sys.version_info < (3, 6): - UTC_NAME = "UTC+00:00" -else: - UTC_NAME = "UTC" +UTC_NAME = "UTC" @pytest.mark.parametrize( diff --git a/tests/test_filesink_retention.py b/tests/test_filesink_retention.py index a8fab8aeb..ef565ca22 100644 --- a/tests/test_filesink_retention.py +++ b/tests/test_filesink_retention.py @@ -115,7 +115,7 @@ def test_not_managed_files(tmp_path): i = logger.add(tmp_path / "test.log", retention=0, catch=False) logger.remove(i) - assert set(f.name for f in tmp_path.iterdir()) == others + assert {f.name for f in tmp_path.iterdir()} == others @pytest.mark.parametrize("filename", ["test", "test.log"]) diff --git a/tests/test_repr.py b/tests/test_repr.py index a705c02fe..2e398eef2 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -1,6 +1,5 @@ import logging import pathlib -import re import sys from loguru import logger @@ -154,12 +153,8 @@ def __repr__(self): def test_standard_handler(): handler = logging.StreamHandler(sys.__stderr__) logger.add(handler) - if sys.version_info >= (3, 6): - r = " (NOTSET)>)]>" - assert repr(logger) == r - else: - r = r"\)\]>" - assert re.match(r, repr(logger)) + r = " (NOTSET)>)]>" + assert repr(logger) == r def test_multiple_handlers():