{"id":28542714,"url":"https://github.com/pomponchik/emptylog","last_synced_at":"2025-10-08T23:33:35.413Z","repository":{"id":221994931,"uuid":"755916856","full_name":"pomponchik/emptylog","owner":"pomponchik","description":"Mimicking the logger protocol","archived":false,"fork":false,"pushed_at":"2025-05-23T06:25:53.000Z","size":157,"stargazers_count":27,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-09-28T03:46:18.931Z","etag":null,"topics":["logging","logging-protocol","mocks","protocols","typing"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pomponchik.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-02-11T13:30:03.000Z","updated_at":"2025-07-10T11:16:00.000Z","dependencies_parsed_at":"2024-02-11T17:31:43.968Z","dependency_job_id":"fbf4375d-31e2-4ae3-9f36-a3a778db1ccd","html_url":"https://github.com/pomponchik/emptylog","commit_stats":null,"previous_names":["pomponchik/emptylog"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/pomponchik/emptylog","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomponchik%2Femptylog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomponchik%2Femptylog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomponchik%2Femptylog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomponchik%2Femptylog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pomponchik","download_url":"https://codeload.github.com/pomponchik/emptylog/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomponchik%2Femptylog/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279000721,"owners_count":26082862,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["logging","logging-protocol","mocks","protocols","typing"],"created_at":"2025-06-09T21:07:21.687Z","updated_at":"2025-10-08T23:33:35.375Z","avatar_url":"https://github.com/pomponchik.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"![logo](https://raw.githubusercontent.com/pomponchik/emptylog/develop/docs/assets/logo_5.svg)\n\n[![Downloads](https://static.pepy.tech/badge/emptylog/month)](https://pepy.tech/project/emptylog)\n[![Downloads](https://static.pepy.tech/badge/emptylog)](https://pepy.tech/project/emptylog)\n[![codecov](https://codecov.io/gh/pomponchik/emptylog/graph/badge.svg?token=I7Be1jVBeB)](https://codecov.io/gh/pomponchik/emptylog)\n[![Lines of code](https://sloc.xyz/github/pomponchik/emptylog/?category=code)](https://github.com/boyter/scc/)\n[![Hits-of-Code](https://hitsofcode.com/github/pomponchik/emptylog?branch=main)](https://hitsofcode.com/github/pomponchik/emptylog/view?branch=main)\n[![Test-Package](https://github.com/pomponchik/emptylog/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/emptylog/actions/workflows/tests_and_coverage.yml)\n[![Python versions](https://img.shields.io/pypi/pyversions/emptylog.svg)](https://pypi.python.org/pypi/emptylog)\n[![PyPI version](https://badge.fury.io/py/emptylog.svg)](https://badge.fury.io/py/emptylog)\n[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n\nThis library is designed to extend the capabilities of the built-in [`logging`](https://docs.python.org/3/library/logging.html) library.\n\nOne of the important problems that it solves is the fact that almost no one tests logging in their programs. Are you sure that your program logs everything you need? Programmers cover with tests what they consider to be the basic logic of the program. Logging problems are usually detected only when something is on fire, and then you realize that there are not enough logs, or the wrong thing is being logged. On the contrary, this library makes logging as much a test-friendly part of your program as regular logic.\n\nHere are some of the features it provides:\n\n- A [universal logger protocol](#universal-logger-protocol) that allows you to replace one logger with another without typing violations. In tests, you can replace the original logger with a [logger that remembers its calls](#memory-logger) to check that logging is correct.\n- An [empty logger]((#empty-logger)) that does nothing when you call it. It is useful for writing library functions where the user can pass their logger, but there is no logging by default.\n- A [memory logger](#memory-logger) that remembers all the times it was called. To verify that your code is correctly logged in, pass it a memory logger object instead of the default logger, and then check how it was used.\n- A [printing logger](#printing-logger) is a \"toy version\" of a real logger that you can use to visualize all logger calls inside your test.\n- All loggers presented in this library can be easily [combined](#summation-of-loggers) using the \"+\" symbol.\n\n\n## Table of contents\n\n- [**Installing**](#installing)\n- [**Universal Logger Protocol**](#universal-logger-protocol)\n- [**Empty Logger**](#empty-logger)\n- [**Memory Logger**](#memory-logger)\n- [**Printing Logger**](#printing-logger)\n- [**Summation of loggers**](#summation-of-loggers)\n\n\n## Installing\n\nInstall it from [Pypi](https://pypi.org/project/emptylog/):\n\n```bash\npip install emptylog\n```\n\nYou can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).\n\n\n## Universal Logger Protocol\n\nEasily check whether an object is a logger using the protocol. The protocol contains 6 classic logger methods:\n\n```python\ndef debug(message: str, *args: Any, **kwargs: Any) -\u003e None: pass\ndef info(message: str, *args: Any, **kwargs: Any) -\u003e None: pass\ndef warning(message: str, *args: Any, **kwargs: Any) -\u003e None: pass\ndef error(message: str, *args: Any, **kwargs: Any) -\u003e None: pass\ndef exception(message: str, *args: Any, **kwargs: Any) -\u003e None: pass\ndef critical(message: str, *args: Any, **kwargs: Any) -\u003e None: pass\n```\n\nThe protocol is verifiable in runtime by the [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance) function. Let's check this on a regular logger from `logging`:\n\n```python\nimport logging\nfrom emptylog import LoggerProtocol\n\nprint(isinstance(logging.getLogger('some_name'), LoggerProtocol))\n#\u003e True\n```\n\nThis also works for third-party loggers with the same signature. Let's try it on [loguru](https://github.com/Delgan/loguru):\n\n```python\nfrom loguru import logger\nfrom emptylog import LoggerProtocol\n\nprint(isinstance(logger, LoggerProtocol))\n#\u003e True\n```\n\nAnd of course, you can use the protocol for type hints:\n\n```python\ndef function(logger: LoggerProtocol):\n    logger.info('There was an earthquake in Japan, check the prices of hard drives!')\n```\n\nThe protocol can be used for static checks by any tool you prefer, such as [`mypy`](https://github.com/python/mypy).\n\n\n## Empty Logger\n\n`EmptyLogger` is the simplest implementation of the [logger protocol](#universal-logger-protocol). When calling logging methods from an object of this class, nothing happens. You can use it as a stub, for example, when defining functions:\n\n```python\nfrom emptylog import EmptyLogger, LoggerProtocol\n\ndef function(logger: LoggerProtocol = EmptyLogger()):\n    logger.error('Kittens have spilled milk, you need to pour more.')\n```\n\n\n## Memory Logger\n\n`MemoryLogger` is a special class designed for tests. Its difference from [`EmptyLogger`](#empty-logger) is that it remembers all the times it was called.\n\nThe call history is stored in the `data` attribute and sorted by logger method names:\n\n```python\nfrom emptylog import MemoryLogger\n\nlogger = MemoryLogger()\n\nlogger.error('Joe Biden forgot his name again.')\nlogger.error('And again.')\nlogger.info(\"Joe, remember, you're Joe.\")\n\nprint(logger.data)\n#\u003e LoggerAccumulatedData(debug=[], info=[LoggerCallData(message=\"Joe, remember, you're Joe.\", args=(), kwargs={})], warning=[], error=[LoggerCallData(message='Joe Biden forgot his name again.', args=(), kwargs={}), LoggerCallData(message='And again.', args=(), kwargs={})], exception=[], critical=[])\n\nprint(logger.data.info[0].message)\n#\u003e Joe, remember, you're Joe.\nprint(logger.data.error[0].message)\n#\u003e Joe Biden forgot his name again.\nprint(logger.data.info[0].args)\n#\u003e ()\nprint(logger.data.info[0].kwargs)\n#\u003e {}\n```\n\nYou can find out the total number of logs saved by `MemoryLogger` by applying the [`len()`](https://docs.python.org/3/library/functions.html#len) function to the `data` attribute:\n\n```python\nlogger = MemoryLogger()\n\nlogger.warning(\"Are you ready, kids?\")\nlogger.info(\"Aye, aye, Captain!\")\nlogger.error(\"I can't hear you!\")\nlogger.info(\"Aye, aye, Captain!\")\nlogger.debug(\"Oh!\")\n\nprint(len(logger.data))\n#\u003e 5\n```\n\n\n## Printing Logger\n\n`PrintingLogger` is the simplest logger designed for printing to the console. You cannot control the format or direction of the output, or send logs to a special microservice that will forward them to a long-term storage with indexing support. No, here you can only get basic output to the console and nothing else. Here is an example:\n\n```python\nfrom emptylog import PrintingLogger\n\nlogger = PrintingLogger()\n\nlogger.debug(\"I ate a strange pill found under my grandfather's pillow.\")\n#\u003e 2024-07-08 20:52:31.342048 | DEBUG     | I ate a strange pill found under my grandfather's pillow.\nlogger.info(\"Everything seems to be fine.\")\n#\u003e 2024-07-08 20:52:31.342073 | INFO      | Everything seems to be fine.\nlogger.error(\"My grandfather beat me up. He seems to be breathing fire.\")\n#\u003e 2024-07-08 20:52:31.342079 | ERROR     | My grandfather beat me up. He seems to be breathing fire.\n```\n\nAs you can see, 3 things are output to the console: the exact time, the logging level, and the message. The message does not support extrapolation. Also, you won't see any additional arguments here that could have been passed to the method.\n\n\u003e ⚠️ Do not use this logger in production. It is intended solely for the purposes of debugging or testing of software.\n\nIf necessary, you can change the behavior of the logger by passing it a callback, which is called for the finished message to print it to the console. Instead of the original function (the usual [`print`](https://docs.python.org/3/library/functions.html#print) function is used under the hood), you can pass something more interesting (the code example uses the [`termcolor`](https://github.com/termcolor/termcolor) library):\n\n```python\nfrom termcolor import colored\n\ndef callback(string: str) -\u003e None:\n    print(colored(string, 'green'), end='')\n\nlogger = PrintingLogger(printing_callback=callback)\n\nlogger.info('Hello, the colored world!')\n#\u003e 2024-07-09 11:20:03.693837 | INFO      | Hello, the colored world!\n# You can't see it here, but believe me, if you repeat the code at home, the output in the console will be green!\n```\n\n\n## Summation of loggers\n\nAll loggers represented in this library can be grouped together. To do this, just use the \"+\" operator:\n\n```python\nfrom emptylog import PrintingLogger, MemoryLogger\n\nlogger = PrintingLogger() + MemoryLogger()\nprint(logger)\n#\u003e LoggersGroup(PrintingLogger(), MemoryLogger())\n```\n\nThe group object also implements the [logger protocol](#universal-logger-protocol). If you use it as a logger, it will alternately call the appropriate methods from the loggers nested in it:\n\n```python\nprinting_logger = PrintingLogger()\nmemory_logger = MemoryLogger()\n\nsuper_logger = printing_logger + memory_logger\n\nsuper_logger.info('Together we are a force!')\n#\u003e 2024-07-10 16:49:21.247290 | INFO      | Together we are a force!\nprint(memory_logger.data.info[0].message)\n#\u003e Together we are a force!\n```\n\nYou can sum up more than 2 loggers. In this case, the number of nesting levels will not grow:\n\n```python\nprint(MemoryLogger() + MemoryLogger() + MemoryLogger())\n#\u003e LoggersGroup(MemoryLogger(), MemoryLogger(), MemoryLogger())\n```\n\nYou can also add any loggers from this library with loggers from other libraries, for example from the [standard library](https://docs.python.org/3/library/logging.html) or from [loguru](https://github.com/Delgan/loguru):\n\n```python\nimport logging\nfrom loguru import logger as loguru_logger\n\nprint(MemoryLogger() + loguru_logger + logging.getLogger(__name__))\n#\u003e LoggersGroup(MemoryLogger(), \u003cloguru.logger handlers=[(id=0, level=10, sink=\u003cstderr\u003e)]\u003e, \u003cLogger __main__ (WARNING)\u003e)\n```\n\nFinally, you can use a group as an iterable object, as well as find out the number of nested loggers in a standard way:\n\n```python\ngroup = PrintingLogger() + MemoryLogger()\n\nprint(len(group))\n#\u003e 2\nprint([x for x in group])\n#\u003e [PrintingLogger(), MemoryLogger()]\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpomponchik%2Femptylog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpomponchik%2Femptylog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpomponchik%2Femptylog/lists"}