pax_global_header00006660000000000000000000000064146330361550014520gustar00rootroot0000000000000052 comment=4dc49d0efabf290c453efb273ccea6096b037330 enrich-1.3.0/000077500000000000000000000000001463303615500127715ustar00rootroot00000000000000enrich-1.3.0/.config/000077500000000000000000000000001463303615500143145ustar00rootroot00000000000000enrich-1.3.0/.config/requirements-test.in000066400000000000000000000001241463303615500203410ustar00rootroot00000000000000pytest-cov>=2.7.1 pytest-mock>=3.3.1 pytest-plus pytest-xdist>=1.29.0 pytest>=5.4.0 enrich-1.3.0/.config/requirements.in000066400000000000000000000001061463303615500173640ustar00rootroot00000000000000rich>=10.0.0,<12.5.0 # https://github.com/pycontribs/enrich/issues/40 enrich-1.3.0/.flake8000066400000000000000000000003721463303615500141460ustar00rootroot00000000000000[flake8] # do not add excludes for files in repo exclude = .venv/,.tox/,dist/,build/,.eggs/ format = pylint # E203: https://github.com/python/black/issues/315 ignore = E741,W503,W504,H,E501,E203,D # 88 is official black default: max-line-length = 88 enrich-1.3.0/.github/000077500000000000000000000000001463303615500143315ustar00rootroot00000000000000enrich-1.3.0/.github/CODEOWNERS000066400000000000000000000000141463303615500157170ustar00rootroot00000000000000* @ssbarnea enrich-1.3.0/.github/FUNDING.yml000066400000000000000000000001001463303615500161350ustar00rootroot00000000000000# These are supported funding model platforms github: ssbarnea enrich-1.3.0/.github/dependabot.yml000066400000000000000000000004321463303615500171600ustar00rootroot00000000000000# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: pip directory: / schedule: interval: daily allow: - dependency-type: "all" assignees: - "ssbarnea" enrich-1.3.0/.github/release-drafter.yml000066400000000000000000000001171463303615500201200ustar00rootroot00000000000000# see https://github.com/ansible/team-devtools _extends: ansible/team-devtools enrich-1.3.0/.github/workflows/000077500000000000000000000000001463303615500163665ustar00rootroot00000000000000enrich-1.3.0/.github/workflows/ack.yml000066400000000000000000000003761463303615500176550ustar00rootroot00000000000000--- # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/ack.yml name: ack on: pull_request_target: types: [opened, labeled, unlabeled, synchronize] jobs: ack: uses: ansible/team-devtools/.github/workflows/ack.yml@main enrich-1.3.0/.github/workflows/push.yml000066400000000000000000000004021463303615500200640ustar00rootroot00000000000000--- # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/push.yml name: push on: push: branches: - main - "releases/**" - "stable/**" jobs: ack: uses: ansible/team-devtools/.github/workflows/push.yml@main enrich-1.3.0/.github/workflows/release.yml000066400000000000000000000024401463303615500205310ustar00rootroot00000000000000name: release on: release: types: [published] jobs: pypi: name: Publish to PyPI registry environment: release runs-on: ubuntu-20.04 env: FORCE_COLOR: 1 PY_COLORS: 1 TOXENV: packaging TOX_PARALLEL_NO_SPINNER: 1 steps: - name: Switch to using Python 3.9 by default uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install tox run: >- python3 -m pip install --user tox - name: Check out src from Git uses: actions/checkout@v2 with: fetch-depth: 0 # needed by setuptools-scm - name: Build dists run: python -m tox - name: Publish to test.pypi.org if: >- # "create" workflows run separately from "push" & "pull_request" github.event_name == 'release' uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.testpypi_password }} repository_url: https://test.pypi.org/legacy/ - name: Publish to pypi.org if: >- # "create" workflows run separately from "push" & "pull_request" github.event_name == 'release' uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.pypi_password }} enrich-1.3.0/.github/workflows/tox.yml000066400000000000000000000036031463303615500177250ustar00rootroot00000000000000--- name: tox on: push: # only publishes pushes to the main branch to TestPyPI branches: # any integration branch but not tag - "main" pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: FORCE_COLOR: 1 # tox, pytest, ansible-lint PY_COLORS: 1 jobs: pre: name: pre runs-on: ubuntu-22.04 outputs: matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: - name: Determine matrix id: generate_matrix uses: coactions/dynamic-matrix@v1 with: min_python: "3.9" platforms: linux,macos macos: minmax other_names: | lint pkg tox: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} needs: pre strategy: matrix: ${{ fromJson(needs.pre.outputs.matrix) }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # we need full depth and tags for versioning - name: Set up stock Python ${{ matrix.python_version }} from GitHub uses: actions/setup-python@v4 with: python-version: ${{ matrix.python_version }} # cache: pip - name: Log the selected Python version info (${{ matrix.python_version }}) run: | python --version --version which python - name: Install tox run: | python -m pip install -U pip pip install "tox>=4.0.0" - name: Run tox -e ${{ matrix.passed_name }} run: tox -e ${{ matrix.passed_name }} check: # This job does nothing and is only used for the branch protection if: always() needs: - tox runs-on: ubuntu-22.04 steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} enrich-1.3.0/.gitignore000066400000000000000000000034361463303615500147670ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ src/enrich/_version.py enrich-1.3.0/.pre-commit-config.yaml000066400000000000000000000034331463303615500172550ustar00rootroot00000000000000--- exclude: | (?x)( ^docs/conf.py$ ) repos: - repo: https://github.com/pycontribs/mirrors-prettier # keep it before yamllint rev: v3.3.2 hooks: - id: prettier always_run: true additional_dependencies: - prettier - prettier-plugin-toml - prettier-plugin-sort-json - 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: fix-byte-order-marker - id: check-executables-have-shebangs - id: check-merge-conflict - id: debug-statements language_version: python3 - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: - id: yamllint 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: - id: mypy # empty args needed in order to match mypy cli behavior args: [--strict] additional_dependencies: - pytest>=6.2.5 - pytest-mock>=3.6.1 - packaging - rich>=11.0.0 - repo: https://github.com/pycqa/pylint rev: v3.2.3 hooks: - id: pylint # see https://github.com/PyCQA/pylint/issues/6535 language_version: "3.10" additional_dependencies: - pytest>=6.2.4 - pytest-mock>=3.6.1 - rich>=11.0.0 - typing - typing-extensions enrich-1.3.0/.yamllint000066400000000000000000000011651463303615500146260ustar00rootroot00000000000000--- # Based on ansible-lint config extends: default ignore: | *{{cookiecutter** rules: braces: max-spaces-inside: 1 level: error brackets: max-spaces-inside: 1 level: error colons: max-spaces-after: -1 level: error commas: max-spaces-after: -1 level: error comments: disable comments-indentation: disable document-start: disable empty-lines: max: 3 level: error hyphens: level: error indentation: disable key-duplicates: enable line-length: disable new-line-at-end-of-file: disable new-lines: type: unix trailing-spaces: disable truthy: disable enrich-1.3.0/LICENSE000066400000000000000000000020561463303615500140010ustar00rootroot00000000000000MIT License Copyright (c) 2020 Sorin Sbarnea Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. enrich-1.3.0/README.md000066400000000000000000000040511463303615500142500ustar00rootroot00000000000000# enrich Enriched extends [rich](https://pypi.org/project/rich/) library functionality with a set of changes that were not accepted to rich itself. ## Console with redirect support Our Console class adds one additional option to rich.Console in order to redirect `sys.stdout` and `sys.stderr` streams using a FileProxy. ```python from enrich.console import Console import sys console = Console( redirect=True, # <-- not supported by rich.console.Console record=True) sys.write("foo") # this assert would have passed without redirect=True assert console.export_text() == "foo" ``` ## Console with implicit soft wrapping If you want to produce fluid terminal output, one where the client terminal decides where to wrap the text instead of the application, you can now tell the Console constructor the soft_wrap preference. ```python from enrich.console import Console import sys console = Console(soft_wrap=True) console.print(...) # no longer need to pass soft_wrap to each print ``` ## Console.print can also deal with ANSI escapes Extends Rich Console to detect if original text already had ANSI escapes and decodes it before processing it. This solves the case where printing output captured from other processes that contained ANSI escapes would brake. [upstream-404](https://github.com/willmcgugan/rich/discussions/404) ## Soft-wrapping logger Rich logger assumes that you always have a fixed width console and it does wrap logged output according to it. Our alternative logger does exactly the opposite: it ignores the columns of the current console and prints output using a Console with soft wrapping enabled. The result are logged lines that can be displayed on any terminal or web page as they will allow the client to decide when to perform the wrapping. ```python import logging from enrich.logging import RichHandler FORMAT = "%(message)s" logging.basicConfig( level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] ) log = logging.getLogger("rich") log.info("Text that we do not want pre-wrapped by logger: %s", 100 * "x") ``` enrich-1.3.0/pyproject.toml000066400000000000000000000057451463303615500157200ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 65.3.0", # required by pyproject+setuptools_scm integration and editable installs "setuptools_scm[toml] >= 7.0.5", # required for "no-local-version" scheme ] build-backend = "setuptools.build_meta" [project] # https://peps.python.org/pep-0621/#readme requires-python = ">=3.9" dynamic = ["version", "dependencies", "optional-dependencies"] name = "enrich" description = "enrich" readme = "README.md" authors = [{ "name" = "Sorin Sbarnea", "email" = "sorin.sbarnea@gmail.com" }] maintainers = [ { "name" = "Sorin Sbarnea", "email" = "sorin.sbarnea@gmail.com" } ] license = { text = "MIT" } classifiers = [ # https://pypi.org/classifiers/ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: POSIX :: Linux", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python", "Topic :: System :: Systems Administration", "Topic :: Utilities", ] keywords = ["console", "logging", "rich"] [project.urls] homepage = "https://github.com/pycontribs/enrich" repository = "https://github.com/pycontribs/enrich" changelog = "https://github.com/pycontribs/enrich/releases" [tool.isort] profile = "black" known_first_party = "subprocess_tee" [tool.mypy] python_version = 3.9 color_output = true error_summary = true disallow_any_generics = true disallow_any_unimported = true disallow_untyped_calls = true disallow_untyped_defs = true warn_redundant_casts = true warn_return_any = true warn_unused_configs = true [tool.pytest] addopts = "--doctest-modules --durations 10 --durations-min=3 --color=yes" doctest_optionflags = ["ALLOW_UNICODE", "ELLIPSIS"] norecursedirs = "dist doc build .tox .eggs" filterwarnings = [ # treat warnings as errors unless we add them below "error" # 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"] } [tool.setuptools_scm] local_scheme = "no-local-version" tag_regex = "^(?Pv)?(?P[0-9.]+)(?P.*)?$" write_to = "src/enrich/_version.py" # To prevent accidental pick of mobile version tags such 'v6' git_describe_command = [ "git", "describe", "--dirty", "--tags", "--long", "--match", "v*.*", ] enrich-1.3.0/src/000077500000000000000000000000001463303615500135605ustar00rootroot00000000000000enrich-1.3.0/src/enrich/000077500000000000000000000000001463303615500150305ustar00rootroot00000000000000enrich-1.3.0/src/enrich/__init__.py000066400000000000000000000000251463303615500171360ustar00rootroot00000000000000"""Enrich module.""" enrich-1.3.0/src/enrich/console.py000066400000000000000000000063761463303615500170600ustar00rootroot00000000000000"""Module that helps integrating with rich library.""" import os import sys from typing import Any, TextIO import rich.console as rich_console from rich.ansi import AnsiDecoder from rich.file_proxy import FileProxy class Console(rich_console.Console): """Extends rich Console class.""" def __init__(self, *args: str, redirect: bool = True, **kwargs: Any) -> None: """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 if "soft_wrap" not in kwargs: kwargs["soft_wrap"] = True # Unless user already mentioning terminal preference, we use our # heuristic to make an informed decision. if "force_terminal" not in kwargs: kwargs["force_terminal"] = should_do_markup( stream=kwargs.get("file", sys.stdout), ) super().__init__(*args, **kwargs) self.extended = True if self.redirect: if not hasattr(sys.stdout, "rich_proxied_file"): sys.stdout = FileProxy(self, sys.stdout) # type: ignore if not hasattr(sys.stderr, "rich_proxied_file"): sys.stderr = FileProxy(self, sys.stderr) # type: ignore # https://github.com/python/mypy/issues/4441 def print(self, *args, **kwargs) -> None: # type: ignore """Print override that respects user soft_wrap preference.""" # Currently rich is unable to render ANSI escapes with print so if # we detect their presence, we decode them. # https://github.com/willmcgugan/rich/discussions/404 if args and isinstance(args[0], str) and "\033" in args[0]: text = format(*args) + "\n" decoder = AnsiDecoder() args = list(decoder.decode(text)) # type: ignore super().print(*args, **kwargs) # Based on Ansible implementation def to_bool(value: Any) -> bool: """Return a bool for the arg.""" if value is None or isinstance(value, bool): return bool(value) if isinstance(value, str): value = value.lower() if value in ("yes", "on", "1", "true", 1): return True return False def should_do_markup(stream: TextIO = sys.stdout) -> bool: """Decide about use of ANSI colors.""" py_colors = None # https://xkcd.com/927/ for env_var in ["PY_COLORS", "CLICOLOR", "FORCE_COLOR", "ANSIBLE_FORCE_COLOR"]: value = os.environ.get(env_var, None) if value is not None: py_colors = to_bool(value) break # If deliverately disabled colors if os.environ.get("NO_COLOR", None): return False # User configuration requested colors if py_colors is not None: return to_bool(py_colors) term = os.environ.get("TERM", "") if "xterm" in term: return True if term == "dumb": return False # Use tty detection logic as last resort because there are numerous # factors that can make isatty return a misleading value, including: # - stdin.isatty() is the only one returning true, even on a real terminal # - stderr returting false if user user uses a error stream coloring solution return stream.isatty() enrich-1.3.0/src/enrich/logging.py000066400000000000000000000065561463303615500170440ustar00rootroot00000000000000"""Implements enriched RichHandler.""" 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 # Based on https://github.com/willmcgugan/rich/blob/master/rich/_log_render.py 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__( # noqa: PLR0913 self, show_time: bool = False, show_level: bool = False, show_path: bool = True, 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: str | None = None def __call__( # pylint: disable=too-many-arguments # noqa: PLR0913 self, console: Console, # noqa: ARG002 renderables: Iterable[ConsoleRenderable], log_time: datetime | None = None, time_format: str | None = None, level: TextType = "", 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(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)) else: result += Text(log_time_display) self._last_time = log_time_display if self.show_level: if not isinstance(level, Text): level = Text(level) # CRITICAL is the longest identifier from default set. if len(level) < 9: # noqa: PLR2004 level += " " * (9 - len(level)) result += level for elem in renderables: result += elem 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 "", ) if line_no: path_text.append(f":{line_no}") result += path_text return result 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 self._log_render = FluidLogRender( show_time=kwargs.get("show_time", False), show_level=kwargs.get("show_level", True), show_path=kwargs.get("show_path", False), omit_repeated_times=kwargs.get("omit_repeated_times", True), ) # type: ignore enrich-1.3.0/src/enrich/py.typed000066400000000000000000000000001463303615500165150ustar00rootroot00000000000000enrich-1.3.0/test/000077500000000000000000000000001463303615500137505ustar00rootroot00000000000000enrich-1.3.0/test/__init__.py000066400000000000000000000000001463303615500160470ustar00rootroot00000000000000enrich-1.3.0/test/test_console.py000066400000000000000000000062771463303615500170370ustar00rootroot00000000000000"""Tests for rich module.""" import io import sys import pytest 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") # 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") 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() # noqa: T201 console = Console(force_terminal=True, record=True, redirect=True) console.print("[green]this from Console.print()[/green]", style="red") text = console.export_text(clear=False) assert "this from Console" in text html = console.export_html(clear=False) assert "#008000" in html 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, ) text = 21 * "x" console.print(text, end="") # pylint: disable=no-member assert console.file.getvalue() == text # type: ignore result = console.export_text() assert text in result def test_console_print_ansi() -> None: """Validates that Console.print() with ANSI does not make break them.""" console = Console(force_terminal=True, record=True, soft_wrap=True, redirect=True) text = "\033[92mfuture is green!\033[0m" console.print(text) text_result = console.export_text(clear=False) assert "future is green!" in text_result html_result = console.export_html() assert "#00ff00" in html_result def test_markup_detection_pycolors0() -> None: """Assure PY_COLORS=0 disables markup.""" with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setenv("PY_COLORS", "0") assert not should_do_markup() def test_markup_detection_pycolors1() -> None: """Assure PY_COLORS=1 enables markup.""" with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setenv("PY_COLORS", "1") assert should_do_markup() def test_markup_detection_tty_yes(mocker: MockFixture) -> None: """Assures TERM=xterm enables markup.""" mocker.patch("sys.stdout.isatty", return_value=True) mocker.patch("os.environ", {"TERM": "xterm"}) assert should_do_markup() mocker.resetall() mocker.stopall() def test_markup_detection_tty_no(mocker: MockFixture) -> None: """Assures that if no tty is reported we disable markup.""" mocker.patch("os.environ", {}) mocker.patch("sys.stdout.isatty", return_value=False) assert not should_do_markup() mocker.resetall() mocker.stopall() if __name__ == "__main__": test_console_print_ansi() enrich-1.3.0/test/test_logging.py000066400000000000000000000050131463303615500170060ustar00rootroot00000000000000"""Tests related to enriched RichHandler""" from __future__ import annotations import io import logging import re import pytest from enrich.console import Console from enrich.logging import RichHandler 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 using UTF-8. """ if isinstance(text, bytes): text = text.decode("utf-8") return re.sub(r"\x1b[^m]*m", "", text) @pytest.fixture(name="rich_logger") def rich_logger_fixture() -> tuple[logging.Logger, RichHandler]: """Returns tuple with logger and handler to be tested.""" rich_handler = RichHandler( console=Console( file=io.StringIO(), force_terminal=True, width=80, color_system="truecolor", soft_wrap=True, ), enable_link_path=False, ) logging.basicConfig( 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: """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 logger.error("%s %s", text, 123) # verify that the long text was not wrapped output = strip_ansi_escape(rich_handler.console.file.getvalue()) # type: ignore assert text in output assert "ERROR" in output assert "\n" not in output[:-1] if __name__ == "__main__": handler = RichHandler( console=Console( force_terminal=True, width=55510, # this is expected to have no effect color_system="truecolor", soft_wrap=True, ), enable_link_path=False, show_time=True, show_level=True, show_path=True, ) logging.basicConfig( level="NOTSET", format="%(message)s", # datefmt="[DATE]", handlers=[handler], ) log = logging.getLogger("rich") # log.addHandler(handler) data = {"foo": "text", "bar": None, "number": 123} log.error("This was a long error") log.warning("This was warning %s apparently", 123) log.info("Having info is good") log.debug("Some kind of debug message %s", None) log.info("Does this dictionary %s render ok?", data) enrich-1.3.0/tox.ini000066400000000000000000000024341463303615500143070ustar00rootroot00000000000000[tox] minversion = 4.0.0b2 envlist = lint pkg py isolated_build = True [testenv] usedevelop = True # do not put * in passenv as it may break builds do to reduced isolation passenv = CI GITHUB_* HOME PIP_* PUBLISH PYTEST_* SSH_AUTH_SOCK TERM setenv = PIP_DISABLE_VERSION_CHECK=1 PYTEST_REQPASS=9 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 commands = python -m pytest extras = rich test allowlist_externals = find rm sh [testenv:lint] description = Runs all linting tasks commands = # to run a single linter you can do "pre-commit run flake8" python -m pre_commit run {posargs:--all} deps = pre-commit>=1.18.1 extras = skip_install = true usedevelop = false [testenv:pkg] description = Do packaging/distribution. If tag is not present or PEP440 compliant upload to PYPI could fail # `usedevelop = true` overrides `skip_install` instruction, it's unwanted usedevelop = false # don't install package in this env skip_install = true deps = build >= 0.9.0 twine >= 3.2.0 # pyup: ignore setenv = commands = rm -rfv {toxinidir}/dist/ python -m build \ --outdir {toxinidir}/dist/ \ {toxinidir} # metadata validation sh -c "python -m twine check --strict {toxinidir}//dist/*"