├── .coafile ├── .github ├── FUNDING.yml └── workflows │ └── semgrep.yml ├── .gitignore ├── .pylintrc ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── circle.yml ├── fast-requirements.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── structlog_pretty ├── __init__.py ├── processors.py └── utils.py ├── test-requirements.txt ├── test ├── __init__.py ├── test_JSONPrettifier.py ├── test_MultilinePrinter.py ├── test_NumericRounder.py ├── test_PathPrettifier.py ├── test_SyntaxHighlighter.py └── test_XMLPrettifier.py └── tox.ini /.coafile: -------------------------------------------------------------------------------- 1 | [Default] 2 | files = **.(py|md|rst|yml), tox.ini, .coafile 3 | ignore = (.tox|env|.env|venv)/** 4 | 5 | indent_size = 4 6 | use_spaces = True 7 | max_line_length = 120 8 | max_lines_per_file = 1000 9 | file_naming_convention = snake 10 | 11 | [filenames] 12 | bears = FilenameBear 13 | files = structlog_pretty/**.py 14 | 15 | [lengths] 16 | bears = LineCountBear, LineLengthBear 17 | 18 | [spacing] 19 | ignore = (.tox|env|.env|venv)/**, **.yml, tox.ini, .coafile 20 | bears = SpaceConsistencyBear 21 | 22 | [config-spacing] 23 | files = **.yml, tox.ini, .coafile 24 | bears = SpaceConsistencyBear 25 | indent_size = 2 26 | 27 | [python-semantic] 28 | files = **.py 29 | bears = RadonBear, PyUnusedCodeBear 30 | language = python 31 | 32 | [yaml] 33 | files = **.(yml|yaml) 34 | bears = YAMLLintBear 35 | 36 | [restructuredtext] 37 | files = **.rst 38 | bears = reSTLintBear 39 | 40 | [commit] 41 | bears = GitCommitBear 42 | shortlog_length = 72 43 | 44 | [keywords] 45 | bears = KeywordBear 46 | keywords, ci_keywords = TODO, FIXME, pdb.set_trace() # Ignore KeywordBear 47 | cs_keywords = 48 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: underyx 2 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | push: 4 | branches: 5 | - main 6 | - master 7 | name: Semgrep 8 | jobs: 9 | semgrep: 10 | name: Scan 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: returntocorp/semgrep-action@v1 15 | with: 16 | auditOn: push 17 | publishToken: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | publishDeployment: 28 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #####=== Python ===##### 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | #####=== Vim ===##### 61 | [._]*.s[a-w][a-z] 62 | [._]s[a-w][a-z] 63 | *.un~ 64 | Session.vim 65 | .netrwhist 66 | *~ 67 | 68 | #####=== VirtualEnv ===##### 69 | # Virtualenv 70 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 71 | .Python 72 | [Bb]in 73 | [Ii]nclude 74 | [Ll]ib 75 | [Ss]cripts 76 | pyvenv.cfg 77 | pip-selfcheck.json 78 | 79 | #####=== OSX ===##### 80 | .DS_Store 81 | .AppleDouble 82 | .LSOverride 83 | 84 | # Icon must end with two \r 85 | Icon 86 | 87 | # Thumbnails 88 | ._* 89 | 90 | # Files that might appear on external disk 91 | .Spotlight-V100 92 | .Trashes 93 | 94 | # Directories potentially created on remote AFP share 95 | .AppleDB 96 | .AppleDesktop 97 | Network Trash Folder 98 | Temporary Items 99 | .apdisk 100 | 101 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | # jobs=0 means 'use all CPUs' 3 | jobs=0 4 | 5 | [MESSAGES CONTROL] 6 | disable = 7 | missing-docstring, 8 | line-too-long, 9 | invalid-name, 10 | no-value-for-parameter, 11 | no-member, 12 | unused-argument, 13 | broad-except, 14 | relative-import, 15 | wrong-import-position, 16 | bare-except, 17 | locally-disabled, 18 | protected-access, 19 | abstract-method, 20 | no-self-use, 21 | fixme, 22 | too-few-public-methods, 23 | 24 | [REPORTS] 25 | output-format=colorized 26 | 27 | [FORMAT] 28 | logging-modules= 29 | logging, 30 | structlog, 31 | 32 | [TYPECHECK] 33 | # pygments: sys.modules hacks causing false positives - https://github.com/PyCQA/pylint/issues/491 34 | ignored-modules= 35 | pygments.lexers, 36 | pygments.formatters, 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.4.3 (2024-07-15) 4 | 5 | - Keep any trailing whitespace passed into SyntaxHighlighter 6 | 7 | ## 0.4.2 (2024-07-15) 8 | 9 | - Fix typing import unavailable on 3.8 10 | - Remove trailing newline added by SyntaxHighlighter 11 | 12 | ## 0.4.1 (2024-06-11) 13 | 14 | - Export PathPrettifier via top-level package 15 | 16 | ## 0.4.0 (2024-06-11) 17 | 18 | - Use orjson for fast JSON prettifying instead of rapidjsonX 19 | - Add PathPrettifier 20 | 21 | ## 0.3.0 (2020-07-08) 22 | 23 | - Fix SyntaxHighlighter - skip code highlighting when there is no code to highlight. 24 | 25 | ## 0.2.0 (2020-07-08) 26 | 27 | - Fix JSONPrettifier, XMLPrettifier - skip code coloring when there is no code to color. 28 | 29 | ## 0.1.1 (2016-12-15) 30 | 31 | - Fix NumericRounder converting booleans into floats 32 | 33 | ## 0.1.0.post1 (2016-11-21) 34 | 35 | No changes, just a re-release due to an issue with the build environment used for the 0.1.0 wheel. 36 | 37 | ## 0.1.0 (2016-11-20) 38 | 39 | Initial release. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bence Nagy (underyx) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | include fast-requirements.txt 4 | include test-requirements.txt 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | structlog-pretty 2 | ================ 3 | 4 | .. image:: https://circleci.com/gh/underyx/structlog-pretty.svg?style=shield 5 | :target: https://circleci.com/gh/underyx/structlog-pretty 6 | :alt: CI Status 7 | 8 | .. image:: https://codecov.io/gh/underyx/structlog-pretty/branch/master/graph/badge.svg 9 | :target: https://codecov.io/gh/underyx/structlog-pretty 10 | :alt: Code Coverage 11 | 12 | A collection of structlog_ processors for prettier output: a code syntax 13 | highlighter, JSON and XML prettifiers, a multiline string printer, and 14 | a numeric value rounder. 15 | 16 | Installation 17 | ------------ 18 | 19 | First of all, sorry, grandma, but ``structlog-pretty`` requires Python 3. 20 | 21 | You can just install the library with pip:: 22 | 23 | pip install structlog-pretty 24 | 25 | or, if you want faster prettifying processors:: 26 | 27 | pip install structlog-pretty[fast] 28 | 29 | The downside of the faster processors is that they will build C extensions and 30 | they need ``libxml`` to be installed. 31 | 32 | Usage 33 | ----- 34 | 35 | Add structlog-pretty processors to your structlog configuration 36 | 37 | .. code-block:: python 38 | 39 | import structlog 40 | import structlog_pretty 41 | 42 | structlog.configure( 43 | # ... 44 | processors=[ 45 | structlog.stdlib.add_log_level, 46 | structlog_pretty.NumericRounder(digits=2, only_fields=['timing']) 47 | structlog.processors.JSONRenderer(), 48 | ], 49 | ) 50 | 51 | A nice example of a processor pipeline for the *prettiest* logs could be 52 | 53 | .. code-block:: python 54 | 55 | processors=[ 56 | # ... 57 | structlog_pretty.JSONPrettifier(['request', 'response']), 58 | structlog_pretty.XMLPrettifier(['soap_response']), 59 | structlog_pretty.PathPrettifier(), 60 | structlog_pretty.SyntaxHighlighter({'request': 'json', 'response': 'json', 'soap_response': 'xml'}), 61 | structlog_pretty.MultilinePrinter(['request', 'response', 'soap_response']), 62 | # ... 63 | ], 64 | 65 | .. _structlog: https://github.com/hynek/structlog 66 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | override: 4 | - pip install coverage==7.* tox==4* tox-pyenv==1.* tox-battery==0.6.* 5 | - pyenv local 3.12.4 3.8.19 6 | - tox --notest 7 | cache_directories: 8 | - .tox 9 | test: 10 | override: 11 | - tox 12 | post: 13 | - bash <(curl -s https://codecov.io/bash) 14 | -------------------------------------------------------------------------------- /fast-requirements.txt: -------------------------------------------------------------------------------- 1 | lxml==5.* 2 | orjson==3.* 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygments==2.* 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | norecursedirs = 3 | .git 4 | dist 5 | build 6 | .tox 7 | venv 8 | .env 9 | env 10 | testpaths = 11 | test 12 | 13 | [coverage:run] 14 | branch = True 15 | source = structlog_pretty 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | from setuptools import setup 3 | 4 | with io.open("requirements.txt") as f: 5 | install_requires = f.read().splitlines() 6 | 7 | with io.open("test-requirements.txt") as f: 8 | tests_require = f.read().splitlines() 9 | 10 | with io.open("fast-requirements.txt") as f: 11 | fast_extra_requires = f.read().splitlines() 12 | 13 | with io.open("README.rst") as f: 14 | long_description = f.read() 15 | 16 | setup( 17 | name="structlog-pretty", 18 | version="0.4.3", 19 | url="https://github.com/underyx/structlog-pretty", 20 | author="Bence Nagy", 21 | author_email="bence@underyx.me", 22 | maintainer="Bence Nagy", 23 | maintainer_email="bence@underyx.me", 24 | download_url="https://github.com/underyx/structlog-pretty/releases", 25 | description="A collection of structlog processors for prettier output", 26 | long_description=long_description, 27 | packages=["structlog_pretty"], 28 | install_requires=install_requires, 29 | tests_require=tests_require, 30 | extras_require={"fast": fast_extra_requires}, 31 | classifiers=[ 32 | "Development Status :: 3 - Alpha", 33 | "License :: OSI Approved :: MIT License", 34 | "Operating System :: OS Independent", 35 | "Programming Language :: Python :: 3 :: Only", 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /structlog_pretty/__init__.py: -------------------------------------------------------------------------------- 1 | from .processors import ( 2 | NumericRounder, 3 | JSONPrettifier, 4 | XMLPrettifier, 5 | SyntaxHighlighter, 6 | MultilinePrinter, 7 | PathPrettifier, 8 | ) 9 | -------------------------------------------------------------------------------- /structlog_pretty/processors.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | from pathlib import Path 4 | import re 5 | import sys 6 | import json 7 | 8 | try: 9 | import orjson 10 | 11 | fast_json_available = True 12 | except ImportError: 13 | fast_json_available = False 14 | 15 | from typing import Optional 16 | from xml.dom.minidom import parseString as parse_xml_string 17 | 18 | try: 19 | from lxml import etree 20 | 21 | fast_xml_available = True 22 | except ImportError: 23 | fast_xml_available = False 24 | 25 | from pygments import highlight 26 | from pygments.lexers import get_lexer_by_name 27 | from pygments.formatters import TerminalFormatter 28 | 29 | from . import utils 30 | 31 | 32 | class NumericRounder(object): 33 | """A processor for rounding numbers in the event values 34 | 35 | For instance, ``1.162537216`` will be changed to ``1.163``. 36 | """ 37 | 38 | def __init__(self, digits=3, only_fields=None): 39 | """Create a processor that rounds numbers in the event values 40 | 41 | :param digits: The number of digits to round to 42 | :param only_fields: An iterable specifying the fields to round 43 | """ 44 | self.digits = digits 45 | try: 46 | self.only_fields = set(only_fields) 47 | except TypeError: 48 | self.only_fields = None 49 | 50 | def __call__(self, _, __, event_dict): 51 | for key, value in event_dict.items(): 52 | if self.only_fields is not None and key not in self.only_fields: 53 | continue 54 | if isinstance(value, bool): 55 | continue # don't convert True to 1.0 56 | 57 | try: 58 | event_dict[key] = round(value, self.digits) 59 | except TypeError: 60 | continue 61 | 62 | return event_dict 63 | 64 | 65 | class JSONPrettifier(object): 66 | """A processor for prettifying JSON strings 67 | 68 | For instance, ``{"numbers":[1,2]}`` will be changed to this:: 69 | 70 | { 71 | "numbers": [ 72 | 1, 73 | 2 74 | ] 75 | } 76 | """ 77 | 78 | def __init__(self, json_fields): 79 | """Create a processor that prettifies JSON strings in the event values 80 | 81 | :param json_fields: An iterable specifying the fields to prettify 82 | """ 83 | self.fields = json_fields 84 | self.prettify = ( 85 | self.fast_prettify if fast_json_available else self.slow_prettify 86 | ) 87 | 88 | @staticmethod 89 | def slow_prettify(code): 90 | return json.dumps(json.loads(code), indent=2) 91 | 92 | @staticmethod 93 | def fast_prettify(code): 94 | return orjson.dumps(orjson.loads(code), option=orjson.OPT_INDENT_2).decode() 95 | 96 | def __call__(self, _, __, event_dict): 97 | for field in self.fields: 98 | try: 99 | code = event_dict[field] 100 | except KeyError: 101 | continue 102 | if not code: 103 | continue 104 | event_dict[field] = self.prettify(code) 105 | 106 | return event_dict 107 | 108 | 109 | class XMLPrettifier(object): 110 | """A processor for prettifying XML strings 111 | 112 | For instance, ```` will be changed to this:: 113 | 114 | 115 | 116 | 117 | 118 | """ 119 | 120 | def __init__(self, xml_fields): 121 | """Create a processor that prettifies XML strings in the event values 122 | 123 | :param xml_fields: An iterable specifying the fields to prettify 124 | """ 125 | self.fields = xml_fields 126 | if fast_xml_available: 127 | self.prettify = self.fast_prettify 128 | self.lxml_parser = etree.XMLParser(remove_blank_text=True) 129 | else: 130 | self.prettify = self.slow_prettify 131 | self.lxml_parser = None 132 | 133 | @staticmethod 134 | def slow_prettify(code): 135 | xml = parse_xml_string(code) 136 | utils.strip_minidom_whitespace(xml) 137 | xml.normalize() 138 | result = xml.toprettyxml(indent=" ") 139 | result = result.replace('\n', "") 140 | return result.strip() 141 | 142 | def fast_prettify(self, code): 143 | result = etree.tostring( 144 | etree.fromstring(code.encode(), parser=self.lxml_parser), pretty_print=True 145 | ) 146 | return result.strip().decode() 147 | 148 | def __call__(self, _, __, event_dict): 149 | for field in self.fields: 150 | try: 151 | code = event_dict[field] 152 | except KeyError: 153 | continue 154 | if not code: 155 | continue 156 | event_dict[field] = self.prettify(code) 157 | 158 | return event_dict 159 | 160 | 161 | class SyntaxHighlighter(object): 162 | """A processor for syntax highlighting code""" 163 | 164 | def __init__(self, field_map): 165 | """Create a processor that syntax highlights code in the event values 166 | 167 | The syntax highlighting will use with ANSI terminal color codes. 168 | 169 | :param field_map: A mapping with field names mapped to languages, e.g. 170 | ``{'body': 'json': 'soap_response': 'xml'}`` 171 | """ 172 | self.lexers = { 173 | field: get_lexer_by_name(language) for field, language in field_map.items() 174 | } 175 | 176 | def __call__(self, _, __, event_dict): 177 | for field, lexer in self.lexers.items(): 178 | try: 179 | code = event_dict[field] 180 | except KeyError: 181 | continue 182 | 183 | if not code: 184 | continue 185 | 186 | trailing_whitespace_match = re.search(r"\s*$", code) 187 | trailing_whitespace = ( 188 | trailing_whitespace_match.group(0) if trailing_whitespace_match else "" 189 | ) 190 | 191 | event_dict[field] = ( 192 | highlight(code, lexer, TerminalFormatter()).rstrip() 193 | + trailing_whitespace 194 | ) 195 | 196 | return event_dict 197 | 198 | 199 | class MultilinePrinter(object): 200 | """A processor for printing multiline strings""" 201 | 202 | def __init__(self, fields, target=sys.stdout): 203 | """Create a processor that prints the requested fields' values 204 | 205 | This is useful for strings with newlines in them. Keep in mind that the 206 | fields will be popped from the event dictionary, so they will not be 207 | visible to anything (other processors and the logger itself) after this 208 | processor has printed them. 209 | 210 | :param fields: An iterable specifying the fields to print 211 | :param target: A file-like object to print to 212 | """ 213 | self.fields = fields 214 | self.target = target 215 | 216 | def __call__(self, _, __, event_dict): 217 | for field in self.fields: 218 | try: 219 | print(event_dict.pop(field), file=self.target, end="") 220 | except KeyError: 221 | continue 222 | 223 | return event_dict 224 | 225 | 226 | class PathPrettifier: 227 | """A processor for printing paths. 228 | 229 | Changes all pathlib.Path objects. 230 | 231 | 1. Remove PosixPath(...) wrapper by calling str() on the path. 232 | 2. If path is relative to current working directory, 233 | print it relative to working directory. 234 | 235 | Note that working directory is determined when configuring structlog. 236 | """ 237 | 238 | def __init__(self, base_dir: Optional[Path] = None): 239 | self.base_dir = base_dir or Path.cwd() 240 | 241 | def __call__(self, _, __, event_dict): 242 | for key, path in event_dict.items(): 243 | if not isinstance(path, Path): 244 | continue 245 | path = event_dict[key] 246 | try: 247 | path = path.relative_to(self.base_dir) 248 | except ValueError: 249 | pass # path is not relative to cwd 250 | event_dict[key] = str(path) 251 | 252 | return event_dict 253 | -------------------------------------------------------------------------------- /structlog_pretty/utils.py: -------------------------------------------------------------------------------- 1 | from xml.dom.minidom import Node 2 | 3 | 4 | def strip_minidom_whitespace(node): 5 | """Strips all whitespace from a minidom XML node and its children 6 | 7 | This operation is made in-place.""" 8 | for child in node.childNodes: 9 | if child.nodeType == Node.TEXT_NODE: 10 | if child.nodeValue: 11 | child.nodeValue = child.nodeValue.strip() 12 | elif child.nodeType == Node.ELEMENT_NODE: 13 | strip_minidom_whitespace(child) 14 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==8.* 2 | pytest-cov==5.* 3 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underyx/structlog-pretty/a6a4abbb1f6e4a879f9f5a95ba067577cea65a08/test/__init__.py -------------------------------------------------------------------------------- /test/test_JSONPrettifier.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import pytest 4 | from structlog_pretty.processors import JSONPrettifier as uut 5 | 6 | 7 | cases = [ 8 | ('null', 'null'), 9 | ('{}', '{}'), 10 | ('{"key": "value"}', dedent(''' 11 | { 12 | "key": "value" 13 | } 14 | ''').strip()), 15 | ('{"key": ["value", "value"]}', dedent(''' 16 | { 17 | "key": [ 18 | "value", 19 | "value" 20 | ] 21 | } 22 | ''').strip()), 23 | ] 24 | modes = ('slow', 'fast') 25 | 26 | 27 | @pytest.mark.parametrize(['mode', 'param', 'expected'], [ 28 | [mode] + list(case) for mode in modes for case in cases 29 | ]) 30 | def test_run(mode, param, expected, monkeypatch): 31 | monkeypatch.setattr('structlog_pretty.processors.fast_json_available', mode == 'fast') 32 | processor = uut(json_fields=['param']) 33 | event_dict = processor(None, None, {'param': param}) 34 | assert event_dict == {'param': expected} 35 | 36 | 37 | @pytest.mark.parametrize(['mode', 'param', 'expected'], [ 38 | (mode, case[0], case[0]) for mode in modes for case in cases 39 | ]) 40 | def test_field_name_setting(mode, param, expected, monkeypatch): 41 | monkeypatch.setattr('structlog_pretty.processors.fast_json_available', mode == 'fast') 42 | processor = uut(json_fields=['not_the_param']) 43 | event_dict = processor(None, None, {'param': param}) 44 | assert event_dict == {'param': expected} 45 | -------------------------------------------------------------------------------- /test/test_MultilinePrinter.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import pytest 4 | from structlog_pretty.processors import MultilinePrinter as uut 5 | 6 | cases = ['', 'foo', 'foo\n', 'foo\n\n\n', 'foo\nbar'] 7 | 8 | 9 | @pytest.mark.parametrize(['param'], [[case] for case in cases]) 10 | def test_run(param): 11 | buffer = io.StringIO() 12 | processor = uut(fields=['param'], target=buffer) 13 | event_dict = processor(None, None, {'param': param}) 14 | assert buffer.getvalue() == param 15 | assert event_dict == {} 16 | 17 | 18 | @pytest.mark.parametrize(['param'], [[case] for case in cases]) 19 | def test_fields_setting(param): 20 | buffer = io.StringIO() 21 | processor = uut(fields=['not_the_param'], target=buffer) 22 | event_dict = processor(None, None, {'param': param}) 23 | assert buffer.getvalue() == '' 24 | assert event_dict == {'param': param} 25 | -------------------------------------------------------------------------------- /test/test_NumericRounder.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | import itertools 3 | 4 | import pytest 5 | from structlog_pretty.processors import NumericRounder as uut 6 | 7 | 8 | @pytest.mark.parametrize(['param', 'expected'], [ 9 | (0.0, 0.0), 10 | (1.0, 1.0), 11 | (1.12345, 1.123), 12 | (1.2345, 1.234), 13 | (-1.12345, -1.123), 14 | (-1.2345, -1.234), 15 | (Decimal('1.12345'), Decimal('1.123')), 16 | (Decimal('-1.12345'), Decimal('-1.123')), 17 | (None, None), 18 | ('str', 'str'), 19 | (True, True), 20 | (False, False), 21 | ]) 22 | def test_run(param, expected): 23 | processor = uut() 24 | event_dict = processor(None, None, {'param': param}) 25 | assert type(event_dict['param']) == type(expected) # pylint: disable=unidiomatic-typecheck 26 | assert event_dict == {'param': expected} 27 | 28 | 29 | @pytest.mark.parametrize(['digits', 'param', 'expected'], [ 30 | (0, 0.0, 0.0), 31 | (10, 0.0, 0.0), 32 | (0, 1.12345, 1.0), 33 | (1, 1.12345, 1.1), 34 | (10, 1.12345, 1.12345), 35 | (0, -1.12345, -1.0), 36 | (1, -1.12345, -1.1), 37 | (10, -1.12345, -1.12345), 38 | (0, Decimal('1.12345'), Decimal('1.0')), 39 | (1, Decimal('1.12345'), Decimal('1.1')), 40 | (10, Decimal('1.12345'), Decimal('1.12345')), 41 | (0, Decimal('-1.12345'), Decimal('-1.0')), 42 | (1, Decimal('-1.12345'), Decimal('-1.1')), 43 | (10, Decimal('-1.12345'), Decimal('-1.12345')), 44 | (0, 'str', 'str'), 45 | (1, 'str', 'str'), 46 | (10, 'str', 'str'), 47 | ]) 48 | def test_digits_setting(digits, param, expected): 49 | processor = uut(digits=digits) 50 | event_dict = processor(None, None, {'param': param}) 51 | assert event_dict == {'param': expected} 52 | 53 | 54 | @pytest.mark.parametrize(['only_fields'], [ 55 | [only_fields] 56 | for i in range(4) 57 | for only_fields in itertools.combinations(['float', 'decimal', 'str'], i) 58 | ]) 59 | def test_only_fields_setting(only_fields): 60 | unrounded = {'float': 1.12345, 'decimal': Decimal('1.12345'), 'str': 'str'} 61 | rounded = {'float': 1.123, 'decimal': Decimal('1.123'), 'str': 'str'} 62 | processor = uut(only_fields=only_fields) 63 | event_dict = processor(None, None, unrounded) 64 | for field in unrounded: 65 | should_be_rounded = field in only_fields 66 | assert event_dict[field] == rounded[field] if should_be_rounded else unrounded[field] 67 | -------------------------------------------------------------------------------- /test/test_PathPrettifier.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from structlog_pretty.processors import PathPrettifier as uut 5 | 6 | 7 | BASE_DIR = Path("/tmp") 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ["param", "expected"], 12 | [ 13 | (Path("/tmp/foo.py"), "foo.py"), 14 | (Path("/tmp/dir/foo.py"), "dir/foo.py"), 15 | (Path("foo.py"), "foo.py"), 16 | (Path("/elsewhere/foo.py"), "/elsewhere/foo.py"), 17 | (1, 1), 18 | (None, None), 19 | ("/tmp/dir/foo.py", "/tmp/dir/foo.py"), 20 | ], 21 | ) 22 | def test_run(param, expected): 23 | processor = uut(BASE_DIR) 24 | event_dict = processor(None, None, {"param": param}) 25 | assert type(event_dict["param"]) == type(expected) # pylint: disable=unidiomatic-typecheck 26 | assert event_dict == {"param": expected} 27 | -------------------------------------------------------------------------------- /test/test_SyntaxHighlighter.py: -------------------------------------------------------------------------------- 1 | import re 2 | from structlog_pretty.processors import SyntaxHighlighter as uut 3 | 4 | 5 | def test_json(): 6 | processor = uut(field_map={"body": "json"}) 7 | event_dict = processor(None, None, {"body": '{"ping": true}'}) 8 | assert "\x1b[" in event_dict["body"], "should have at least one ANSI escape code" 9 | assert not event_dict["body"].endswith( 10 | "\n" 11 | ), "should not have trailing newline added" 12 | 13 | 14 | def test_retain_whitespace(): 15 | processor = uut(field_map={"body": "json"}) 16 | event_dict = processor(None, None, {"body": '{"ping": true}\n\n'}) 17 | match = re.search(r"\s*$", event_dict["body"]) 18 | assert match is not None 19 | assert match.group() == "\n\n" 20 | 21 | 22 | def test_missing_json(): 23 | processor = uut(field_map={"body": "json"}) 24 | event_dict = processor(None, None, {"not_body": '{"ping": true}'}) 25 | assert event_dict["not_body"] == '{"ping": true}' 26 | 27 | 28 | def test_multiple_fields(): 29 | processor = uut(field_map={"body": "json", "body_2": "json"}) 30 | event_dict = processor(None, None, {"body": "null", "body_2": "null"}) 31 | assert "\x1b[" in event_dict["body"], "should have at least one ANSI escape code" 32 | assert "\x1b[" in event_dict["body_2"], "should have at least one ANSI escape code" 33 | 34 | 35 | def test_multiple_languages(): 36 | processor = uut(field_map={"body": "json", "body_2": "xml"}) 37 | event_dict = processor(None, None, {"body": "null", "body_2": ""}) 38 | assert "\x1b[" in event_dict["body"], "should have at least one ANSI escape code" 39 | assert "\x1b[" in event_dict["body_2"], "should have at least one ANSI escape code" 40 | -------------------------------------------------------------------------------- /test/test_XMLPrettifier.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import pytest 4 | from structlog_pretty.processors import XMLPrettifier as uut 5 | 6 | cases = [ 7 | ('', ''), 8 | ('', ''), 9 | ('', dedent(''' 10 | 11 | 12 | 13 | ''').strip()), 14 | ('', dedent(''' 15 | 16 | 17 | 18 | 19 | ''').strip()), 20 | ( 21 | dedent(''' 22 | 23 | 24 | 25 | 26 | ''').strip(), 27 | dedent(''' 28 | 29 | 30 | 31 | 32 | ''').strip(), 33 | ), 34 | ] 35 | modes = ('slow', 'fast') 36 | 37 | 38 | @pytest.mark.parametrize(['mode', 'param', 'expected'], [ 39 | [mode] + list(case) for mode in modes for case in cases 40 | ]) 41 | def test_run(mode, param, expected, monkeypatch): 42 | monkeypatch.setattr('structlog_pretty.processors.fast_xml_available', mode == 'fast') 43 | processor = uut(xml_fields=['param']) 44 | event_dict = processor(None, None, {'param': param}) 45 | assert event_dict == {'param': expected} 46 | 47 | 48 | @pytest.mark.parametrize(['mode', 'param', 'expected'], [ 49 | (mode, case[0], case[0]) for mode in modes for case in cases 50 | ]) 51 | def test_field_name_setting(mode, param, expected, monkeypatch): 52 | monkeypatch.setattr('structlog_pretty.processors.fast_xml_available', mode == 'fast') 53 | processor = uut(xml_fields=['not_the_param']) 54 | event_dict = processor(None, None, {'param': param}) 55 | assert event_dict == {'param': expected} 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = tests-py{38,312} 3 | 4 | [testenv] 5 | usedevelop=True 6 | deps = 7 | -rrequirements.txt 8 | -rtest-requirements.txt 9 | -rfast-requirements.txt 10 | commands = 11 | tests: pytest {posargs:} --cov 12 | --------------------------------------------------------------------------------