diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d364809..8c4eb7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,10 +17,10 @@ jobs: TOX_PARALLEL_NO_SPINNER: 1 steps: - - name: Switch to using Python 3.8 by default + - name: Switch to using Python 3.9 by default uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install tox run: >- python3 -m diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index f75bda8..54d5836 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -26,7 +26,9 @@ jobs: id: generate_matrix uses: coactions/dynamic-matrix@v1 with: + min_python: "3.9" platforms: linux,macos + macos: minmax other_names: | lint pkg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc4c9fc..78ec025 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ exclude: | repos: - repo: https://github.com/pycontribs/mirrors-prettier # keep it before yamllint - rev: v3.3.1 + rev: v3.3.2 hooks: - id: prettier always_run: true @@ -14,36 +14,17 @@ repos: - prettier - prettier-plugin-toml - prettier-plugin-sort-json - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - repo: https://github.com/psf/black - rev: 24.4.2 - hooks: - - id: black - language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks.git rev: v4.6.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - id: mixed-line-ending - - id: check-byte-order-marker + - id: fix-byte-order-marker - id: check-executables-have-shebangs - id: check-merge-conflict - id: debug-statements language_version: python3 - - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - additional_dependencies: - - pydocstyle>=5.1.1 - - flake8-absolute-import - - flake8-black>=0.1.1 - - flake8-docstrings>=1.5.0 - language_version: python3 - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: @@ -51,6 +32,16 @@ repos: files: \.(yaml|yml)$ types: [file, yaml] entry: yamllint --strict + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.4.7" + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + language_version: python3 - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.10.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index e653183..530c296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] # https://peps.python.org/pep-0621/#readme -requires-python = ">=3.8" +requires-python = ">=3.9" dynamic = ["version", "dependencies", "optional-dependencies"] name = "enrich" description = "enrich" @@ -29,7 +29,6 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Operating System :: POSIX", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -52,7 +51,7 @@ profile = "black" known_first_party = "subprocess_tee" [tool.mypy] -python_version = 3.8 +python_version = 3.9 color_output = true error_summary = true disallow_any_generics = true @@ -73,6 +72,26 @@ filterwarnings = [ # ignore::UserWarning ] +[tool.ruff] +target-version = "py39" +# Same as Black. +line-length = 88 + +lint.ignore = [ + "D203", # incompatible with D211 + "D213", # incompatible with D212 + "E501", # we use black + "ANN", + "FBT001", + "FBT002", + "FBT003", + "PGH", +] +lint.select = ["ALL"] + +[tool.ruff.lint.per-file-ignores] +"test/**/*.py" = ["D", "ERA", "S"] + [tool.setuptools.dynamic] dependencies = { file = [".config/requirements.in"] } optional-dependencies.test = { file = [".config/requirements-test.in"] } diff --git a/src/enrich/__init__.py b/src/enrich/__init__.py index e69de29..303dfb5 100644 --- a/src/enrich/__init__.py +++ b/src/enrich/__init__.py @@ -0,0 +1 @@ +"""Enrich module.""" diff --git a/src/enrich/console.py b/src/enrich/console.py index 73a2482..a56dd00 100644 --- a/src/enrich/console.py +++ b/src/enrich/console.py @@ -13,8 +13,9 @@ class Console(rich_console.Console): """Extends rich Console class.""" def __init__(self, *args: str, redirect: bool = True, **kwargs: Any) -> None: - """ - enrich console does soft-wrapping by default and this diverge from + """Enrich constructor. + + Enrich console does soft-wrapping by default and this diverge from original rich console which does not, creating hard-wraps instead. """ self.redirect = redirect @@ -26,7 +27,7 @@ def __init__(self, *args: str, redirect: bool = True, **kwargs: Any) -> None: # heuristic to make an informed decision. if "force_terminal" not in kwargs: kwargs["force_terminal"] = should_do_markup( - stream=kwargs.get("file", sys.stdout) + stream=kwargs.get("file", sys.stdout), ) super().__init__(*args, **kwargs) diff --git a/src/enrich/logging.py b/src/enrich/logging.py index e7b9e39..0060d26 100644 --- a/src/enrich/logging.py +++ b/src/enrich/logging.py @@ -1,12 +1,16 @@ -"""Implements enriched RichHandler""" +"""Implements enriched RichHandler.""" -from datetime import datetime -from typing import TYPE_CHECKING, Any, Iterable, Optional +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any from rich.logging import RichHandler as OriginalRichHandler from rich.text import Text, TextType if TYPE_CHECKING: + from collections.abc import Iterable + from rich.console import Console, ConsoleRenderable @@ -15,7 +19,7 @@ class FluidLogRender: # pylint: disable=too-few-public-methods """Renders log by not using columns and avoiding any wrapping.""" # pylint: disable=too-many-arguments - def __init__( + def __init__( # noqa: PLR0913 self, show_time: bool = False, show_level: bool = False, @@ -23,28 +27,30 @@ def __init__( time_format: str = "[%x %X]", omit_repeated_times: bool = True, ) -> None: + """Construcs instance.""" self.show_time = show_time self.show_level = show_level self.show_path = show_path self.time_format = time_format self.omit_repeated_times = omit_repeated_times - self._last_time: Optional[str] = None + self._last_time: str | None = None - def __call__( # pylint: disable=too-many-arguments + def __call__( # pylint: disable=too-many-arguments # noqa: PLR0913 self, - console: "Console", - renderables: Iterable["ConsoleRenderable"], - log_time: Optional[datetime] = None, - time_format: Optional[str] = None, + console: Console, # noqa: ARG002 + renderables: Iterable[ConsoleRenderable], + log_time: datetime | None = None, + time_format: str | None = None, level: TextType = "", - path: Optional[str] = None, - line_no: Optional[int] = None, - link_path: Optional[str] = None, + path: str | None = None, + line_no: int | None = None, + link_path: str | None = None, ) -> Text: + """Call.""" result = Text() if self.show_time: if log_time is None: - log_time = datetime.now() + log_time = datetime.now(tz=timezone.utc) log_time_display = log_time.strftime(time_format or self.time_format) + " " if self.omit_repeated_times and log_time_display == self._last_time: result += Text(" " * len(log_time_display)) @@ -55,7 +61,7 @@ def __call__( # pylint: disable=too-many-arguments if not isinstance(level, Text): level = Text(level) # CRITICAL is the longest identifier from default set. - if len(level) < 9: + if len(level) < 9: # noqa: PLR2004 level += " " * (9 - len(level)) result += level @@ -65,7 +71,8 @@ def __call__( # pylint: disable=too-many-arguments if self.show_path and path: path_text = Text(" ", style="repr.filename") path_text.append( - path, style=f"link file://{link_path}" if link_path else "" + path, + style=f"link file://{link_path}" if link_path else "", ) if line_no: path_text.append(f":{line_no}") @@ -78,6 +85,7 @@ class RichHandler(OriginalRichHandler): """Enriched handler that does not wrap.""" def __init__(self, *args: Any, **kwargs: Any) -> None: + """Create the handler.""" super().__init__(*args, **kwargs) # RichHandler constructor does not allow custom renderer # https://github.com/willmcgugan/rich/issues/438 diff --git a/test/test_console.py b/test/test_console.py index 31f99b3..43d8ad3 100644 --- a/test/test_console.py +++ b/test/test_console.py @@ -4,29 +4,28 @@ import sys import pytest -from pytest_mock import MockFixture - from enrich.console import Console, should_do_markup +from pytest_mock import MockFixture # pylint: disable=wrong-import-order def test_rich_console_ex() -> None: """Validate that ConsoleEx can capture output from print() calls.""" console = Console(record=True, redirect=True) console.print("alpha") - print("beta") + print("beta") # noqa: T201 sys.stdout.write("gamma\n") sys.stderr.write("delta\n") # While not supposed to happen we want to be sure that this will not raise # an exception. Some libraries may still sometimes send bytes to the # streams, notable example being click. - # sys.stdout.write(b"epsilon\n") # type: ignore + # sys.stdout.write(b"epsilon\n") text = console.export_text() assert text == "alpha\nbeta\ngamma\ndelta\n" def test_rich_console_ex_ansi() -> None: """Validate that ANSI sent to sys.stdout does not become garbage in record.""" - print() + print() # noqa: T201 console = Console(force_terminal=True, record=True, redirect=True) console.print("[green]this from Console.print()[/green]", style="red") @@ -40,7 +39,11 @@ def test_rich_console_ex_ansi() -> None: def test_console_soft_wrap() -> None: """Assures long prints on console are not wrapped when requested.""" console = Console( - file=io.StringIO(), width=20, record=True, soft_wrap=True, redirect=False + file=io.StringIO(), + width=20, + record=True, + soft_wrap=True, + redirect=False, ) text = 21 * "x" console.print(text, end="") diff --git a/test/test_logging.py b/test/test_logging.py index cf6cf87..e089c55 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -1,17 +1,17 @@ """Tests related to enriched RichHandler""" +from __future__ import annotations + import io import logging import re -from typing import Tuple, Union import pytest - from enrich.console import Console from enrich.logging import RichHandler -def strip_ansi_escape(text: Union[str, bytes]) -> str: +def strip_ansi_escape(text: str | bytes) -> str: """Remove all ANSI escapes from string or bytes. If bytes is passed instead of string, it will be converted to string @@ -24,7 +24,7 @@ def strip_ansi_escape(text: Union[str, bytes]) -> str: @pytest.fixture(name="rich_logger") -def rich_logger_fixture() -> Tuple[logging.Logger, RichHandler]: +def rich_logger_fixture() -> tuple[logging.Logger, RichHandler]: """Returns tuple with logger and handler to be tested.""" rich_handler = RichHandler( console=Console( @@ -38,16 +38,18 @@ def rich_logger_fixture() -> Tuple[logging.Logger, RichHandler]: ) logging.basicConfig( - level="NOTSET", format="%(message)s", datefmt="[DATE]", handlers=[rich_handler] + level="NOTSET", + format="%(message)s", + datefmt="[DATE]", + handlers=[rich_handler], ) rich_log = logging.getLogger("rich") rich_log.addHandler(rich_handler) return (rich_log, rich_handler) -def test_logging(rich_logger: Tuple[logging.Logger, RichHandler]) -> None: +def test_logging(rich_logger: tuple[logging.Logger, RichHandler]) -> None: """Test that logger does not wrap.""" - (logger, rich_handler) = rich_logger text = 10 * "x" # a long text that would likely wrap on a normal console