diff --git a/.ackrc b/.ackrc deleted file mode 100644 index 04a3b29..0000000 --- a/.ackrc +++ /dev/null @@ -1,5 +0,0 @@ ---ignore-dir=.venv ---ignore-dir=bin ---ignore-dir=htmlcov ---ignore-dir=log ---ignore-dir=test/acceptance/cassettes diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index b621e71..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @github/annotated-logger-reviewers diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index abf0c1c..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,51 +0,0 @@ ---- -version: 2 -registries: - ghcr: # Define access for a private registry - type: docker-registry - url: ghcr.io - username: PAT - password: ${{secrets.CONTAINER_BUILDER_TOKEN}} -updates: -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: "weekly" - day: "sunday" - time: "21:00" - commit-message: - prefix: "[actions] " - include: "scope" - groups: - dev-dependencies: - patterns: - - "*" # A wildcard that matches all dependencies in the package -- package-ecosystem: "docker" - directory: "/" - registries: - - ghcr # Allow version updates for dependencies in this registry - schedule: - interval: "weekly" - day: "sunday" - time: "21:00" - commit-message: - prefix: "[docker] " - include: "scope" - groups: - dev-dependencies: - patterns: - - "*" # A wildcard that matches all dependencies in the package -- package-ecosystem: pip - directory: "/" - schedule: - interval: "weekly" - day: "sunday" - time: "21:00" - commit-message: - prefix: "[pip] " - prefix-development: "[pip][dev] " - include: "scope" - groups: - dev-dependencies: - patterns: - - "*" # A wildcard that matches all dependencies in the package diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml deleted file mode 100644 index 14f70e9..0000000 --- a/.github/workflows/coverage.yaml +++ /dev/null @@ -1,23 +0,0 @@ - -name: Post Coverage Commit - -on: - workflow_run: - workflows: ["Pytest"] - types: - - completed - -jobs: - coverage: - runs-on: ubuntu-latest - if: github.event.workflow_run.event == "pull_request" && github.event.workflow_run.conclusion == "success" - permissions: - pull-requests: write - contents: write - actions: read - steps: - - name: Python Coverage Comment - uses: py-cov-action/python-coverage-comment-action@b2eb38dd175bf053189b35f738f9207278b00925 - with: - GITHUB_TOKEN: ${{ github.token }} - GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/matchers/ruff.json b/.github/workflows/matchers/ruff.json deleted file mode 100644 index a9a5917..0000000 --- a/.github/workflows/matchers/ruff.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "problemMatcher": [ - { - "owner": "ruff", - "pattern": [ - { - "regexp": "^(Would reformat): (.+)$", - "message": 1, - "file": 2 - } - ] - } - ] -} diff --git a/.github/workflows/publish-to-pypi.yaml b/.github/workflows/publish-to-pypi.yaml deleted file mode 100644 index b52b874..0000000 --- a/.github/workflows/publish-to-pypi.yaml +++ /dev/null @@ -1,120 +0,0 @@ -name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI - -on: push - -permissions: - contents: read - -jobs: - build: - name: Build distribution 📦 - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install hatch - run: >- - python3 -m - pip install - hatch - --user - - name: Build a binary wheel and a source tarball - run: python3 -m hatch build - - name: Store the distribution packages - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - publish-to-pypi: - name: >- - Publish Python 🐍 distribution 📦 to PyPI - if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes - needs: - - build - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/annotated-logger - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 - - github-release: - name: >- - Sign the Python 🐍 distribution 📦 with Sigstore - and upload them to GitHub Release - needs: - - publish-to-pypi - runs-on: ubuntu-latest - - permissions: - contents: write # IMPORTANT: mandatory for making GitHub Releases - id-token: write # IMPORTANT: mandatory for sigstore - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 #v3.0.0 - with: - inputs: >- - ./dist/*.tar.gz - ./dist/*.whl - - name: Create GitHub Release - env: - GITHUB_TOKEN: ${{ github.token }} - run: >- - gh release create - '${{ github.ref_name }}' - --repo '${{ github.repository }}' - --notes "" - - name: Upload artifact signatures to GitHub Release - env: - GITHUB_TOKEN: ${{ github.token }} - # Upload to GitHub Release using the `gh` CLI. - # `dist/` contains the built packages, and the - # sigstore-produced signatures and certificates. - run: >- - gh release upload - '${{ github.ref_name }}' dist/** - --repo '${{ github.repository }}' - - publish-to-testpypi: - name: Publish Python 🐍 distribution 📦 to TestPyPI - needs: - - build - runs-on: ubuntu-latest - - environment: - name: testpypi - url: https://test.pypi.org/p/annotated-logger - - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Publish distribution 📦 to TestPyPI - uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 - with: - repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/pyright.yaml b/.github/workflows/pyright.yaml deleted file mode 100644 index 166e4c0..0000000 --- a/.github/workflows/pyright.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Pyright - -on: - pull_request: - push: - branches: [ main ] - -jobs: - pyright: - permissions: - contents: read - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install hatch - hatch env create dev - - run: echo "$(hatch env find dev)/bin" >> $GITHUB_PATH - - name: Run pyright - uses: jakebailey/pyright-action@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml deleted file mode 100644 index 981b51e..0000000 --- a/.github/workflows/pytest.yaml +++ /dev/null @@ -1,76 +0,0 @@ -name: Pytest - -on: - pull_request: - push: - branches: [ main ] - - -jobs: - pytest: - permissions: - contents: read - - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install hatch - hatch env create dev - - name: Test with pytest - run: | - # Need relative files for the action to report, but it messes up mutmut - echo "[run]" >> .coveragerc - echo "relative_files = true" >> .coveragerc - - hatch run dev:pytest - env: - COVERAGE_FILE: ".coverage.${{ matrix.os }}.${{ matrix.python-version }}" - - name: Store coverage file - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ matrix.os }}-${{ matrix.python-version }} - path: .coverage.${{ matrix.os }}.${{ matrix.python-version }} - include-hidden-files: true - - coverage: - name: Coverage - runs-on: ubuntu-latest - needs: pytest - permissions: - pull-requests: write - contents: write - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - id: download - with: - pattern: coverage-* - merge-multiple: true - - name: Re-add relative so the action is happy - run: | - # Need relative files for the action to report, but it messes up mutmut - echo "[run]" >> .coveragerc - echo "relative_files = true" >> .coveragerc - - name: Python Coverage Comment - uses: py-cov-action/python-coverage-comment-action@b2eb38dd175bf053189b35f738f9207278b00925 - with: - GITHUB_TOKEN: ${{ github.token }} - MERGE_COVERAGE_FILES: true - - name: Store Pull Request comment to be posted - uses: actions/upload-artifact@v4 - if: steps.coverage_comment.outputs.COMMENT_FILE_WRITTEN == 'true' - with: - name: python-coverage-comment-action - path: python-coverage-comment-action.txt - include-hidden-files: true diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml deleted file mode 100644 index 0c27096..0000000 --- a/.github/workflows/ruff.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Ruff - -on: - pull_request: - push: - branches: [ main ] - -jobs: - ruff: - permissions: - contents: read - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install hatch - hatch env create dev - - name: Lint with Ruff (check) - run: | - hatch run dev:ruff check --output-format=github . - - name: Register problem matcher for ruff format - run: echo "::add-matcher::.github/workflows/matchers/ruff.json" - - name: Lint with Ruff (format) - run: | - hatch run dev:ruff format --check . diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f09d704..0000000 --- a/.gitignore +++ /dev/null @@ -1,137 +0,0 @@ -# twirp/proto files: ignore every generated file. keep files inside /rpc/protos -rpc/**/*.py -rpc/**/*.go -rpc/**/*.proto -!rpc/protos/* -go/ - -# 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 - -# 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/ - -# mutmut -.mutmut-cache -html/ - -# Misc -TODO.md -junit.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 4616e88..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,32 +0,0 @@ -repos: -- repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.8.6 - hooks: - # Run the linter. - - id: ruff - # Run the formatter. - - id: ruff-format -- repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.390 - hooks: - - id: pyright - # I don't love having to specify these here, but pre-commit only seems to work - # if you have a venv and hatch doesn't do that - additional_dependencies: ["makefun", "python-json-logger", "pytest", "pychoir"] -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 # Use the ref you want to point at - hooks: - - id: trailing-whitespace - - id: check-merge-conflict - - id: check-toml - - id: check-yaml - - id: debug-statements - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.0 - hooks: - - id: check-dependabot - - id: check-github-actions - - id: check-github-workflows diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 6dc4b12..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,74 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 18065b4..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,52 +0,0 @@ -## Contributing - -[fork]: https://github.com/github/annotated-logger/fork -[pr]: https://github.com/github/annotated-logger/compare -[style]: https://docs.astral.sh/ruff/ - -Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. - -Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.txt). - -Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. - -## Developing a fix/feature - -Annotated Logger uses `ruff`, `pytest`, `pyright` and `mutmut` for testing and linting. It uses [`hatch`](https://github.com/pypa/hatch) as a project manager to build and install dependencies. When developing locally it's suggested that you ensure that your editor supports `ruff` and `pyright` for inline linting. The `pytest` test suite is very quick and should be run frequently. (`mutmut`)[https://github.com/boxed/mutmut] is a mutation testing tool and is fairly slow as it runs the other three tools hundreds of times after making minor tweaks to the code. It will typically be run only once development is complete to ensure everything is fully tested. - -`script/mutmut_runner` is what `mutmut` uses to see if the mutation fails, however, it's also quite useful on it's own as it runs `ruff`, `pytest` and `pyright` exiting as soon as anything fails, so it makes a good sanity check. - -In addition to the tests and linting above all PRs will compare the version number in \_\_init\_\_.py with the version in `main` to ensure that new PRs results in new versions. - -## Submitting a pull request - -1. [Fork][fork] and clone the repository -1. Configure and install the dependencies: `script/bootstrap` -1. Make sure the tests pass on your machine: `hatch run dev:test` -1. Make sure linting passes: `hatch run dev:lint` and `hatch run dev:typing` -1. Create a new branch: `git checkout -b my-branch-name` -1. Make your change, add tests, and make sure the tests still pass -1. Push to your fork and [submit a pull request][pr] -1. Pat yourself on the back and wait for your pull request to be reviewed and merged. - -Here are a few things you can do that will increase the likelihood of your pull request being accepted: - -- Follow the [style guide][style]. -- Write tests. -- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. -- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). - -## Release a Version -Releasing a version is as simple as creating and pushing a tag. A few rules are enforced for tags, the tag must be signed, they cannot be updated or deleted and `Coverage`, `pyright` and `ruff` must be passing (Coverage ensures the pytest matrix also passed). - -Simply run `git tag --sign v0.0.0` (inserting the correct version). Then, `git push origin v0.0.0`. CI will build, publish to Pypi and then create a GitHub Release with the artifacts. - -Technically, the tag name does not matter, it's not used for the version published, that pulls from `__init__.py`. But, it is how anyone will be able to browse the code at a particular version, so it should be set correctly. - -All commits pushed to the repo will also be built and pushed to testpypi. This CI job will fail if the version already exists there. During development set a version in the pattern of `0.0.0.dev0` and increment `dev0` for every new release you'd like to test externally. Then, you can [install the package from testpypi](https://packaging.python.org/en/latest/guides/using-testpypi/). Once the release is finalized, remove the `dev` from the version and ensure the version is updated following [semver](https://semver.org/) by bumping the major, minor or patch version as appropriate. - -## Resources - -- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) -- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) -- [GitHub Help](https://help.github.com) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d1a87c3..0000000 --- a/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM python:3.13-bullseye -ARG DEV_FLAG -COPY /script/ /app/script -WORKDIR /app - -RUN apt-get -y update && \ - apt-get -y install python3-pip && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# We need the whole app before we bootstrap, because we're installing the app -# So, there's no point copying the pipfile over first -COPY . /app - -RUN DEV_FLAG=$DEV_FLAG script/bootstrap_python - - -HEALTHCHECK --interval=5m --timeout=20s --start-period=30s \ - CMD curl -f -XPOST -H 'Content-Type: application/json' -d '{}' http://localhost:8080/health || exit 1 - -EXPOSE 8080 - -CMD "echo 'Nothing to do, this is a package. Dockerfile exists for CI.'" diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 28a50fa..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright GitHub, Inc. - -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. diff --git a/README.md b/README.md index 6fbbd34..76e14fd 100644 --- a/README.md +++ b/README.md @@ -1,272 +1,49 @@ -# Annotated Logger +# Repository Coverage -[contribution]: https://github.com/github/annotated-logger/blob/main/CONTRIBUTING.md +[Full report](https://htmlpreview.github.io/?https://github.com/github/annotated-logger/blob/python-coverage-comment-action-data/htmlcov/index.html) -[![Coverage badge](https://github.com/github/annotated-logger/raw/python-coverage-comment-action-data/badge.svg)](https://github.com/github/annotated-logger/tree/python-coverage-comment-action-data) [![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) [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/) +| Name | Stmts | Miss | Cover | Missing | +|---------------------------------- | -------: | -------: | -------: | --------: | +| annotated\_logger/\_\_init\_\_.py | 314 | 0 | 100% | | +| annotated\_logger/filter.py | 31 | 0 | 100% | | +| annotated\_logger/mocks.py | 124 | 0 | 100% | | +| annotated\_logger/plugins.py | 92 | 0 | 100% | | +| example/\_\_init\_\_.py | 0 | 0 | 100% | | +| example/actions.py | 19 | 0 | 100% | | +| example/api.py | 40 | 0 | 100% | | +| example/calculator.py | 106 | 0 | 100% | | +| example/default.py | 53 | 0 | 100% | | +| example/invalid\_order.py | 5 | 0 | 100% | | +| example/logging\_config.py | 37 | 0 | 100% | | +| **TOTAL** | **821** | **0** | **100%** | | --The `annotated-logger` package provides a decorator that can inject an annotatable logger object into a method or class. This logger object is a drop in replacement for `logging.logger` with additional functionality. You can read about the Annotated Logger in the [announcement blog post](https://github.blog/developer-skills/programming-languages-and-frameworks/introducing-annotated-logger-a-python-package-to-aid-in-adding-metadata-to-logs/). -## Install +## Setup coverage badge -`pip install annotated-logger` +Below are examples of the badges you can use in your main branch `README` file. -## Background +### Direct image -Annotated Logger is actively used by GitHub's Vulnerability Management team to help to easily add context to our logs in splunk. It is more or less feature complete for our current use cases, but we will add additional features/fixes as we discover a need for them. But, we'd love feature requests, bug report and or PRs for either (see our [contribution guidelines][contribution] for more information if you wish to contribute). +[![Coverage badge](https://raw.githubusercontent.com/github/annotated-logger/python-coverage-comment-action-data/badge.svg)](https://htmlpreview.github.io/?https://github.com/github/annotated-logger/blob/python-coverage-comment-action-data/htmlcov/index.html) -## Requirements -Annotated Logger is a Python package. It should work on any version of Python 3, but it is currently tested on 3.9 and higher. +This is the one to use if your repository is private or if you don't want to customize anything. -## Usage +### [Shields.io](https://shields.io) Json Endpoint -The `annotated-logger` package allows you to decorate a function so that the start and end of that function is logged as well as allowing that function to request an `annotated_logger` object which can be used as if it was a standard python `logger`. Additionally, the `annotated_logger` object will have added annotations based on the method it requested from, any other annotations that were configured ahead of time and any annotations that were added prior to a log being made. Finally, any uncaught exceptions in a decorated method will be logged and re-raised, which allows you to when and how a method ended regardless of if it was successful or not. +[![Coverage badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/github/annotated-logger/python-coverage-comment-action-data/endpoint.json)](https://htmlpreview.github.io/?https://github.com/github/annotated-logger/blob/python-coverage-comment-action-data/htmlcov/index.html) -```python -from annotated_logger import AnnotatedLogger +Using this one will allow you to [customize](https://shields.io/endpoint) the look of your badge. +It won't work with private repositories. It won't be refreshed more than once per five minutes. -annotated_logger = AnnotatedLogger( - annotations={"this": "will show up in every log"}, -) -annotate_logs = annotated_logger.annotate_logs +### [Shields.io](https://shields.io) Dynamic Badge -@annotate_logs() -def foo(annotated_logger, bar): - annotated_logger.annotate(bar=bar) - annotated_logger.info("Hi there!", extra={"mood": "happy"}) +[![Coverage badge](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=coverage&query=%24.message&url=https%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fannotated-logger%2Fpython-coverage-comment-action-data%2Fendpoint.json)](https://htmlpreview.github.io/?https://github.com/github/annotated-logger/blob/python-coverage-comment-action-data/htmlcov/index.html) -foo("this is the bar parameter") +This one will always be the same color. It won't work for private repos. I'm not even sure why we included it. -{"created": 1708476277.102495, "levelname": "INFO", "name": "annotated_logger.fe18537a-d293-45d7-83c9-51dab3a4c436", "message": "Hi there!", "mood": "happy", "action": "__main__:foo", "this": "will show up in every log", "bar": "this is the bar parameter", "annotated": true} -{"created": 1708476277.1026022, "levelname": "INFO", "name": "annotated_logger.fe18537a-d293-45d7-83c9-51dab3a4c436", "message": "success", "action": "__main__:foo", "this": "will show up in every log", "bar": "this is the bar parameter", "run_time": "0.0", "success": true, "annotated": true} -``` +## What is that? -The example directory has a few files that exercise all of the features of the annotated-logger package. The `Calculator` class is the most fully featured example (but not a fully featured calculator :wink:). The `logging_config` example shows how to configure a logger via a dictConfig, like django uses. It also shows some of the interactions that can exist between a `logging` logger and an `annotated_logger` if `logging` is configured to use the annotated logger filter. - -Here is a more complete example that makes use of a number of the features. - -```python -import os -from annotated_logger import AnnotatedLogger -al = AnnotatedLogger( - name="annotated_logger.example", - annotations={"branch": os.environ.get("BRANCH", "unknown-branch")} -) -annotate_logs = al.annotate_logs - -@annotate_logs() -def split_username(annotated_logger, username): - annotated_logger.annotate(username=username) - annotated_logger.info("This is a very important message!", extra={"important": True}) - return list(username) -``` -``` ->>> split_username("crimsonknave") -{"created": 1733349907.7293086, "levelname": "DEBUG", "name": "annotated_logger.example.c499f318-e54b-4f54-9030-a83607fa8519", "message": "start", "action": "__main__:split_username", "branch": "unknown-branch", "annotated": true} -{"created": 1733349907.7296104, "levelname": "INFO", "name": "annotated_logger.example.c499f318-e54b-4f54-9030-a83607fa8519", "message": "This is a very important message!", "important": true, "action": "__main__:split_username", "branch": "unknown-branch", "username": "crimsonknave", "annotated": true} -{"created": 1733349907.729843, "levelname": "INFO", "name": "annotated_logger.example.c499f318-e54b-4f54-9030-a83607fa8519", "message": "success", "action": "__main__:split_username", "branch": "unknown-branch", "username": "crimsonknave", "success": true, "run_time": "0.0", "count": 12, "annotated": true} -['c', 'r', 'i', 'm', 's', 'o', 'n', 'k', 'n', 'a', 'v', 'e'] ->>> ->>> split_username(1) -{"created": 1733349913.719831, "levelname": "DEBUG", "name": "annotated_logger.example.1c354f32-dc76-4a6a-8082-751106213cbd", "message": "start", "action": "__main__:split_username", "branch": "unknown-branch", "annotated": true} -{"created": 1733349913.719936, "levelname": "INFO", "name": "annotated_logger.example.1c354f32-dc76-4a6a-8082-751106213cbd", "message": "This is a very important message!", "important": true, "action": "__main__:split_username", "branch": "unknown-branch", "username": 1, "annotated": true} -{"created": 1733349913.7200255, "levelname": "ERROR", "name": "annotated_logger.example.1c354f32-dc76-4a6a-8082-751106213cbd", "message": "Uncaught Exception in logged function", "exc_info": "Traceback (most recent call last):\n File \"/home/crimsonknave/code/annotated-logger/annotated_logger/__init__.py\", line 758, in wrap_function\n result = wrapped(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue]\n File \"\", line 5, in split_username\nTypeError: 'int' object is not iterable", "action": "__main__:split_username", "branch": "unknown-branch", "username": 1, "success": false, "exception_title": "'int' object is not iterable", "annotated": true} -Traceback (most recent call last): - File "", line 1, in - File "", line 2, in split_username - File "/home/crimsonknave/code/annotated-logger/annotated_logger/__init__.py", line 758, in wrap_function - result = wrapped(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue] - File "", line 5, in split_username -TypeError: 'int' object is not iterable -``` - -There are a few things going on in this example. Let's break it down piece by piece. -* The Annotated Logger requires a small amount of setup to use; specifically, you need to instantiate an instance of the `AnnotatedLogger` class. This class contains all of the configuration for the loggers. - * Here we set the name of the logger. (You will need to update the logging config if your name does not start with `annotated_logger` or there will be nothing configured to log your messages.) - * We also set a `branch` annotation that will be sent with all log messages. -* After that, we create an alias for the decorator. You don't have to do this, but I find it's easier to read than `@al.annotate_logs()`. -* Now, we decorate and define our method, but this time we're going to ask the decorator to provide us with a logger object, `annotated_logger`. This `annotated_logger` variable can be used just like a standard `logger` object, but has some extra features. - * This `annotated_logger` argument is added by the decorator before calling the decorated method. - * The signature of the decorated method is adjusted so that it does not have an `annotated_logger` parameter (see how it's called with just name). - * There are optional parameters to the decorator that allow type hints to correctly parse the modified signature. -* We make use of one of those features right away by calling the `annotate` method, which will add whatever kwargs we pass to the `extra` field of all log messages that use the logger. - * Any field added as an annotation will be included in each subsequent log message that uses that logger. - * You can override an annotation by annotating again with the same name -* At last, we send a log message! In this message we also pass in a field that's only for that log message, in the same way you would when using `logger`. -* In the second call, we passed an int to the name field and `list` threw an exception. - * This exception is logged automatically and then re-raised. - * This makes it much easier to know if/when a method ended (unless the process was killed). - -Let's break down each of the fields in the log message: -| Field | Source | Description | -|--------------|-----------------------------|-----------------------------------------------------------------------------| -| created | `logging` | Standard `Logging` field. | -| levelname | `logging` | Standard `Logging` field. | -| name | `annotated_logger` | Logger name (set via class instantiation). | -| message | `logging` | Standard `Logging` field for log content. | -| action | `annotated_logger` | Method name the logger was created for. | -| branch | `AnnotatedLogger()` | Set from the configuration's `branch` annotation. | -| annotated | `annotated_logger` | Boolean indicating if the message was sent via Annotated Logger. | -| important | `annotated_logger.info` | Annotation set for a specific log message. | -| username | `annotated_logger.annotate` | Annotation set by user. | -| success | `annotated_logger` | Indicates if the method completed successfully (`True`/`False`). | -| run_time | `annotated_logger` | Duration of the method execution. | -| count | `annotated_logger` | Length of the return value (if applicable). | - -The `success`, `run_time` and `count` fields are added automatically to the message ("success") that is logged after a decorated method is completed without an exception being raised. - -## Features -### Primary Interactions -The Annotated Logger interacts with `Logging` via two main classes: `AnnotatedAdapter` and `AnnotatedFilter`. `AnnotatedAdapter` is a subclass of `logging.LoggerAdapter` and is what all `annotated_logger` arguments are instances of. `AnnotatedFilter` is a subclass of `logging.Filter` and is where the annotations are actually injected into the log messages. As a user outside of config and plugins, the only part of the code you will only interact with are AnnotatedAdapter in methods and the decorator itself. Each instance of the AnnotatedAdapter class has an `AnnotatedFilter` instance, the `AnnotatedAdapter.annotate` method passes those annotations on to the filter where they are stored. When a message is logged, that filter will calculate all the annotations it should have and then update the existing LogRecord object with those annotations. - -Because each invocation of a method gets its own AnnotatedAdapter object it also has its own AnnotatedFilter object. This ensures that there is no leaking of annotations from one method call to another. - -### Type Hinting -The Annotated Logger is fully type hinted internally and fully supports type hinting of decorated methods. But a little bit of additional detail is required in the decorator invocation. The `annotate_logs` method takes a number of optional arguments. For type hinting, `_typing_self`, `_typing_requested`, `_typing_class` and `provided` are relevant. The three arguments that start with `_typing` have no impact on the behavior of the decorator and are only used in method signature overrides for type hinting. Setting `provided` to `True` tells the decorator that the `annotated_logger` should not be created and will be provided by the caller (thus the signature shouldn't be altered). - -`_typing_self` defaults to `True` as that is how most of my code is written. `provided`, `_typing_class` and `_typing_requested` default to `False`. - -```python -class Example: - @annotate_logs(_typing_requested=True) - def foo(self, annotated_logger): - ... - -e = Example() -e.foo() -``` - -### Plugins -There are a number of plugins that come packaged with the Annotated Logger. Plugins allow for the user to hook into two places: when an exception is caught by the decorator and when logging a message. You can create your own plugin by creating a class that defines the `filter` and `uncaught_exception` methods (or inherits from `annotated_logger.plugins.BasePlugin` which provides noop methods for both). - -The `filter` method of a plugin is called when a message is being logged. Plugins are called in the order they are set in the config. They are called by the AnnotatedFilter object of the AnnotatedAdapter and work like any `logging.Filter`. They take a record argument which is a `logging.LogRecord` object. They can manipulate that record in any way they want and those modifications will persist. Additionally, just like any logging filter, they can stop a message from being logged by returning `False`. - -The `uncaught_exception` method of a plugin is called when the decorator catches an exception in the decorated method. It takes two arguments, `exception` and `logger`. The `logger` argument is the `annotated_logger` for the decorated method. This allows the plugin to annotate the log message stating that there was an uncaught exception that is about to be logged once the plugins have all processed their `uncaught_exception` methods. - -Here is an example of a simple plugin. The plugin inherits from the `BasePlugin`, which isn't strictly needed here since it implements both `filter` and `uncaught_exception`, but if it didn't, inheriting from the `BasePlugin` means that it would fall back to the default noop methods. The plugin has an init so that it can take and store arguments. The `filter` and `uncaught_exception` methods will end up with the same result: `flagged=True` being set if a word matches. But they do it slightly differently, `filter` is called while a given log message is being processed and so the annotation it adds is directly to that record. While `uncaught_exception` is called if an exception is raised and not caught during the execution of the decorated method, so it doesn't have a specific log record to interact with and set an annotation on the logger. The only difference in outcome would be if another plugin emitted a log message during its `uncaught_exception` method after `FlagWordPlugin`, in that case, the additional log message would also have `flagged=True` on it. -```python -from annotated_logger.plugins import BasePlugin - -class FlagWordPlugin(BasePlugin): - """Plugin that flags any log message/exception that contains a word in a list.""" - def __init__(self, *wordlist): - """Save the wordlist.""" - self.wordlist = wordlist - - def filter(self, record): - """Add annotation if the message contains words in the wordlist.""" - for word in self.wordlist: - if word in record.msg: - record.flagged = True - - def uncaught_exception(self, exception, logger): - """Add annotation if exception title contains words in the wordlist.""" - for word in self.wordlist: - if word in str(exception) - logger.annotate(flagged=True) - - -AnnotatedLogger(plugins=[FlagWordPlugin("danger", "Will Robinson")]) -``` - -Plugins are stored in a list and the order they are added can matter. The `BasePlugin` is always the first plugin in the list; any that are set in configuration are added after it. - -When a log message is being sent the `filter` methods of each plugin will be called in the order they appear in the list. Because the `filter` methods often modify the record directly, one filter can break another if, for example, one filter removed or renamed a field that another filter used. Conversely, one filter could expect another to have added or altered a field before its run and would fail if it was ahead of the other filter. Finally, just like in the `logging` module, the `filter` method can stop a log from being emitted by returning False. As soon as a filter does so the processing ends and any Plugins later in the list will not have their `filter` methods called. - -If the decorated method raises an exception that is not caught, then the plugins will again execute in order. The most common interaction is plugins attempting to set/modify the same annotation. The `BasePlugin` and `RequestsPlugin` both set the `exception_title` annotation. Since the `BasePlugin` is always first, the title it sets will be overridden. Other interactions would be one plugin setting an annotation before or after another plugin that emits a log message or sends data to a third-party. In both of those cases the order will impact if the annotation is present or not. - -Plugins that come with the Annotated Logger: -* `GitHubActionsPlugin` - Set a level of log messages to also be emitted in actions notation (`notice::`). -* `NameAdjusterPlugin` - Add a pre/postfix to a name to avoid collisions in any log processing software (`source` is a field in Splunk, but we often include it as a field and it's just hidden). -* `RemoverPlugin` - Remove a field. Exclude `password`/`key` fields and set an object's attributes to the log if you want or ignore fields like `taskName` that are set when running async, but not sync. -* `NestedRemoverPlugin` - Remove a field no matter how deep in a dictionary it is. -* `RenamerPlugin` - Rename one field to another (don't like `levelname` and want `level`, this is how you do that). -* `RequestsPlugin` - Adds a title and status code to the annotations if the exception inherits from `requests.exceptions.HTTPError`. -* `RuntimeAnnotationsPlugin` - Sets dynamic annotations. - -### dictConfig -When adding the Annotated Logger to an existing project, or one that uses other packages that log messages (flask, django and so on), you can configure all of the Annotated Logger via [`dictConfig`](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) by supplying a dictConfig compliant dictionary as the `config` argument when initializing the Annotated Logger class. If, instead, you wish to do this yourself you can pass `config=False` and reference `annotated_logger.DEFAULT_LOGGING_CONFIG` to obtain the config that is used when none is provided and alter/extract as needed. - -There is one special case where the Annotated Logger will modify the config passed to it: if there is a filter named `annotated_filter` that entry will be replaced with a reference to a filter that is created by the instance of the Annotated Logger that's being created. This allows any annotations or other options set to be applied to messages that use that filter. You can instead create a filter that uses the AnnotatedFilter class, but it won't have any of the config the rest of your logs have. - -##### Notes -`dictConfig` partly works when merging dictionaries. I have found that some parts of the config are not overwritten, but other parts seem to lose their references. So, I would encourage you to build up a logging config for everything and call it once only. If you pass `config`, the Annotated Logger will call `logging.config.dictConfig` on your config after it has the option to add/adjust the config. - -The [`logging_config.py`](https://github.com/github/annotated-logger/blob/main/example/logging_config.py) example has a much more detailed breakdown and set of examples. - -### Pytest mock -Included with the package is a pytest mock to assist in testing for logged messages. I know that there are some strong opinions about testing log messages, and I don't suggest doing it extensively, or frequently, but sometimes it's the easiest way to check a loop, or the log message is tied to an alert, and it is important how it's formatted. In these cases, you can ask for the `annotated_logger_mock` fixture which will intercept, record and forward all log messages. - -```python -def test_logs(annotated_logger_mock): - with pytest.raises(KeyError): - complicated_method() - annotated_logger_mock.assert_logged( - "ERROR", # Log level - "That's not the right key", # Log message - present={"success": False, "key": "bad-key"}, # annotations and their values that are required - absent=["fake-annotations"], # annotations that are forbidden - count=1 # Number of times log messages should match - ) -``` - -The `assert_logged` method makes use of [`pychoir`](https://pypi.org/project/pychoir/) for flexible matching. None of the parameters are required, so feel free to use whichever makes sense. Below is a breakdown of the default and valid values for each parameter. - -| Parameter | Default Value | Valid Values | Description | -|-----------|------------------------|------------------------------------------------|-----------------------------------------------------------------| -| `level` | Matches anything | String or string-based matcher | Log level to check (e.g., "ERROR"). | -| `message` | Matches anything | String or string-based matcher | Log message to check. | -| `present` | `{}` | Dictionary with string keys and any value | Annotations required in the log. | -| `absent` | `set()` | `"ALL"`,` set`, or `list` of strings | Annotations that must not be present in the log. | -| `count` | All positive integers | Integer or integer-based matcher | Number of times the log message should match. | - -The `present` key is often what makes the mock truly useful. It allows you to require the things you care about and ignore the things you don't care about. For example, nobody wants their tests to fail because the `run_time` of a method went from `0.0` to `0.1` or fail because the hostname is different on different test machines. But both of those are useful things to have in the logs. This mock should replace everything you use the `caplog` fixture for and more. - -### Other features -##### Class decorators and persist -Classes can be decorated with `@annotate_logs` as well. These classes will have an `annotated_logger` attribute added after the init (I was unable to get it to work inside the `__init__`). Any decorated methods of that class will have an `annotated_logger` that's based on the class logger. Calls to `annotate` that pass `persist=True` will set the annotations on the class Annotated Logger and so subsequent calls of any decorated method of that instance will have those annotations. The class instance's `annotated_logger` will also have an annotation of `class` specifying which class the logs are coming from. - -##### Iterators -The Annotated Logger also supports logging iterations of an `enumerable` object. `annotated_logger.iterator` will log the start, each step of the iteration, and when the iteration is complete. This can be useful for pagination in an API if your results object is enumerable, logging each time a page is fetched instead of sitting for a long time with no indication if the pages are hanging or there are simply many pages. - -By default the `iterator` method will log the value of each iteration, but this can be disabled by setting `value=False`. You can also specify the level to log the iterations at if you don't want the default of `info`. - -##### Provided -Because each decorated method gets its own `annotated_logger` calls to other methods will not have any annotations from the caller. Instead of simply passing the `annotated_logger` object to the method being called, you can specify `provided=True` in the decorator invocation. This does two things: first, it means that this method won't have an `annotated_logger` created and passed automatically, instead it requires that the first argument be an existing `annotated_logger`, which it will use as a basis for the `annotated_logger` object it creates for the function. Second, it adds the annotation of `subaction` and sets the decorated function's name as its value, the `action` annotation is preserved as from the method that called and provided the `annotated_logger`. Annotations are not persisted from a method decorated with `provided=True` to the method that called it, unless the class of the calling method was decorated and the called action annotated with `persist=True`, in which case the annotation is set on the `annotated_logger` of the instance and shared with all methods as is normal for decorated classes. - -The most common use of this is with private methods, especially ones created during a refactor to extract some self contained logic. But other uses are for common methods that are called from a number of different places. - -##### Split Messages -Long messages wreak havoc on log parsing tools. I've encountered cases where the HTML of a 500 error page was too long for Splunk to parse, causing the entire log entry to be discarded and its annotations to go unprocessed. Setting `max_length` when configuring the Annotated Logger will break long messages into multiple log messages each annotated with `split=True`, `split_complete=False`, `message_parts=#` and `message_part=#`. The last part of the long message will have `split_complete=True` when it is logged. - -Only messages can be split like this; annotations will not trigger the splitting. However, a plugin could truncate any values with a length over a certain size. - -##### Pre/Post Hooks -You can register hooks that are executed before and after the decorated method is called. The `pre_call` and `post_call` parameters of the decorator take a reference to a function and will call that function right before passing in the same arguments that the function will be/was called with. This allows the hooks to add annotations and/or log anything that is desired (assuming the decorated function requested an `annotated_logger`). - -Examples of this would be having a set of annotations that annotate fields on a model and a `pre_call` that sets them in a standard way. Or a `post_call` that logs if the function left a model in an unsaved state. - -##### Runtime Annotations -Most annotations are static, but sometimes you need something that's dynamic. These are achieved via the `RuntimeAnnotationsPlugin` in the Annotated Logger config. The `RuntimeAnnotationsPlugin` takes a dict of names and references to functions. These functions will be called and passed the log record when the plugin's filter method is invoked just before the log message is emitted. Whatever is returned by the function will be set as the value of the annotation of the log message currently being logged. - -A common use case is to annotate a request/correlation id, which identifies all of the log messages that were part of a given API request. For Django, one way to do this is via [`django-guid`](https://pypi.org/project/django-guid/). - -### Tips, Tricks And Gotchas -* When using the decorator in more than one file, it's useful to do all of the configuration in a file like `log.py`. That allows you to `from project.log import annotate_logs` everywhere you want to use it and you know it's all configured and everything will be using the same setup. -* Namespacing your loggers helps when there are two projects that both use the Annotated Logger (a package and a service that uses the package). If you are setting anything via `dictConfig` you will want to have a single config that has everything for all Annotated Loggers. -* In addition to setting a correlation id for the API request being processed, passing the correlation id of the caller and then annotating that will allow you to trace from the logs of service A to the specific logs in Service B that relate to a call made by service A. -* Plugins are very flexible. For example: - * Send every `exception` log message to a service like Sentry. - * Suppress logs from another package like Django, that you don't want to see (assuming you've configured Django's logs to use a filter for your Annotated Logger). - * Add annotations for extra information about specific types of exceptions (see the `RequestsPlugin`). - * Set run time annotations on a subset of messages (instead of all messages with `RuntimeAnnotationsPlugin`) - -## License - -This project is licensed under the terms of the MIT open source license. Please refer to MIT for the full terms. - -## Maintainers -This project is primarily maintained by `crimsonknave` on behalf of GitHub's Vulnerability Management team as it was initially developed for our internal use. - -## Support - -Reported bugs will be addressed, pull requests are welcome, but there is limited bandwidth for work on new features. +This branch is part of the +[python-coverage-comment-action](https://github.com/marketplace/actions/python-coverage-comment) +GitHub Action. All the files in this branch are automatically generated and may be +overwritten at any moment. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index abe011d..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,31 +0,0 @@ -Thanks for helping make GitHub safe for everyone. - -# Security - -GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). - -Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation. - -## Reporting Security Issues - -If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure. - -**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** - -Instead, please send an email to opensource-security[@]github.com. - -Please include as much of the information listed below as you can to help us better understand and resolve the issue: - - * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -## Policy - -See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms) diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index 7f18213..0000000 --- a/SUPPORT.md +++ /dev/null @@ -1,13 +0,0 @@ -# Support - -## How to file issues and get help - -This project uses GitHub issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue. - -For help or questions about using this project, please create an issue or discussion. - -Annotated Logger is under active development and maintained by GitHub staff **AND THE COMMUNITY**. We will do our best to respond to support, feature requests, and community questions in a timely manner. - -## GitHub Support Policy - -Support for this project is limited to the resources listed above. diff --git a/annotated_logger/__init__.py b/annotated_logger/__init__.py deleted file mode 100644 index 8608b13..0000000 --- a/annotated_logger/__init__.py +++ /dev/null @@ -1,966 +0,0 @@ -from __future__ import annotations - -import contextlib -import inspect -import logging -import logging.config -import time -import uuid -from collections.abc import Iterator -from copy import copy, deepcopy -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Concatenate, - Literal, - ParamSpec, - Protocol, - TypeVar, - cast, - overload, -) - -from makefun import wraps - -from annotated_logger.filter import AnnotatedFilter -from annotated_logger.plugins import BasePlugin - -if TYPE_CHECKING: # pragma: no cover - from collections.abc import MutableMapping - -# Use 0.0.0.dev1 and so on when working in a PR -# Each push attempts to upload to testpypi, but it only works with a unique version -# https://test.pypi.org/project/annotated-logger/ -# The dev versions in testpypi can then be pulled in to whatever project needed -# the new feature. -VERSION = "1.2.3" # pragma: no mutate - -T = TypeVar("T") -P = ParamSpec("P") -P2 = ParamSpec("P2") -P3 = ParamSpec("P3") -R = TypeVar("R") -S = TypeVar("S") -S2 = TypeVar("S2") -C_co = TypeVar("C_co", covariant=True) - - -class AnnotatedClass(Protocol[C_co]): - """Protocol for typing classes that we annotate and add the logger to.""" - - annotated_logger: AnnotatedAdapter - - -PreCall = Callable[Concatenate[S, "AnnotatedAdapter", P], None] | None -PostCall = Callable[Concatenate[S, "AnnotatedAdapter", P], None] | None -SelfLoggerAndParams = Callable[Concatenate[S, "AnnotatedAdapter", P], R] -LoggerAndParams = Callable[Concatenate["AnnotatedAdapter", P], R] -SelfAndParams = Callable[Concatenate[S, P], R] -ParamsOnly = Callable[P, R] -SelfAndLogger = Callable[[S, "AnnotatedAdapter"], R] -LoggerOnly = Callable[["AnnotatedAdapter"], R] -SelfOnly = Callable[[S], R] -Empty = Callable[[], R] - -NoInjectionSelf = Callable[[SelfAndParams[S, P, R]], SelfAndParams[S, P, R]] -NoInjectionBare = Callable[[ParamsOnly[P, R]], ParamsOnly[P, R]] -InjectionSelf = Callable[[SelfLoggerAndParams[S, P, R]], SelfAndParams[S, P, R]] -InjectionSelfProvide = Callable[ - [SelfLoggerAndParams[S, P, R]], SelfLoggerAndParams[S, P, R] -] -InjectionBare = Callable[[LoggerAndParams[P, R]], ParamsOnly[P, R]] -InjectionBareProvide = Callable[[LoggerAndParams[P, R]], LoggerAndParams[P, R]] - -Function = ( - SelfLoggerAndParams[S, P, R] - | SelfAndParams[S, P, R] - | SelfAndLogger[S, R] - | SelfOnly[S, R] - | LoggerAndParams[P, R] - | ParamsOnly[P, R] - | LoggerOnly[R] - | Empty[R] -) -Decorator = ( - NoInjectionSelf[S, P, R] - | InjectionSelf[S, P, R] - | InjectionSelfProvide[S, P, R] - | NoInjectionBare[P, R] - | InjectionBare[P, R] - | InjectionBareProvide[P, R] -) -Annotations = dict[str, Any] - - -DEFAULT_LOGGING_CONFIG = { - "version": 1, - "disable_existing_loggers": False, # pragma: no mutate - "filters": { - "annotated_filter": { - "annotated_filter": True, # pragma: no mutate - } - }, - "handlers": { - "annotated_handler": { - "class": "logging.StreamHandler", - "formatter": "annotated_formatter", - }, - }, - "formatters": { - "annotated_formatter": { - "class": "pythonjsonlogger.json.JsonFormatter", # pragma: no mutate - "format": "{created} {levelname} {name} {message}", # pragma: no mutate - "style": "{", - }, - }, - "loggers": { - "annotated_logger": { - "level": "DEBUG", - "handlers": ["annotated_handler"], - "propagate": False, # pragma: no mutate - }, - }, -} - - -class AnnotatedIterator(Iterator[T]): - """Iterator that logs as it iterates.""" - - def __init__( - self, - logger: AnnotatedAdapter, - name: str, - wrapped: Iterator[T], - *, - value: bool, - level: str, - ) -> None: - """Store the wrapped iterator, the logger and note if we log the value.""" - self.wrapped = wrapped - self.logger = logger - self.extras: dict[str, T | str] = {"iterator": name} - self.value = value - log_methods = { - "debug": self.logger.debug, - "info": self.logger.info, - "warning": self.logger.warning, - "error": self.logger.error, - "exception": self.logger.exception, - } - self.log_method = log_methods[level] - - def __iter__(self) -> AnnotatedIterator[T]: - """Log the start of the iteration.""" - self.log_method("Starting iteration", extra=self.extras) - return self - - def __next__(self) -> T: - """Log that we are at the next iteration.""" - try: - value = next(self.wrapped) - if self.value: - self.extras["value"] = value - except StopIteration: - self.log_method("Execution complete", extra=self.extras) - raise - - self.log_method("next", extra=self.extras) - return value - - -class AnnotatedAdapter(logging.LoggerAdapter): # pyright: ignore[reportMissingTypeArgument] - """Adapter that provides extra methods.""" - - def __init__( - self, - logger: logging.Logger, - annotated_filter: AnnotatedFilter, - max_length: int | None = None, - ) -> None: - """Adapter that acts like a LogRecord, but allows for annotations.""" - self.filter = annotated_filter - self.logger = logger - self.logger.addFilter(annotated_filter) - self.max_length = max_length - - # We don't need to send in contextual information here - # as we do it in the filter for runtime stuff - super().__init__(logger) - - def iterator( - self, - name: str, - wrapped: Iterator[T], - *, - value: bool = True, - level: str = "info", - ) -> AnnotatedIterator[T]: - """Return an iterator that logs as it iterates.""" - return AnnotatedIterator(self, name, wrapped, value=value, level=level) - - def process( - self, msg: str, kwargs: MutableMapping[str, Any] - ) -> tuple[str, MutableMapping[str, Any]]: - """Override default LoggerAdapter process behavior. - - By default a LoggerAdapter replaces the extras passed in a logger call with - the ones given at it's initialization. That's not the behavior we want. - So, we just return the kwargs as provided instead. - - 3.13 adds a `merge_extra` argument which should make this method unneeded. - But, it doesn't make sense to force everyone to use python 3.13. - """ - return msg, kwargs - - def log( - self, - level: int, - msg: object, - *args: object, - **kwargs: object, - ) -> None: - """Override log method to allow for message splitting.""" - if not self.max_length or not isinstance(msg, str): - return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType] - - msg_len = len(msg) # pyright: ignore[reportArgumentType] - if msg_len <= self.max_length: - return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType] - - msg_chunks = [] - while len(msg) > self.max_length: # pyright: ignore[reportArgumentType] # pragma: no mutate - msg_chunks.append(msg[: self.max_length]) # pyright: ignore[reportArgumentType] - msg = msg[self.max_length :] # pyright: ignore[reportArgumentType] - kwargs["extra"] = {"message_parts": len(msg_chunks) + 1, "split": True} - kwargs["extra"]["split_complete"] = False - for i, part in enumerate(msg_chunks): - kwargs["extra"]["message_part"] = i + 1 - super().log( - level, - part, - *args, - **kwargs, # pyright: ignore[reportArgumentType] - ) - kwargs["extra"]["message_part"] = len(msg_chunks) + 1 - kwargs["extra"]["split_complete"] = True - return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType] - - def annotate(self, *, persist: bool = False, **kwargs: Any) -> None: - """Add an annotation to the filter.""" - if persist: - self.filter.class_annotations.update(kwargs) - else: - self.filter.annotations.update(kwargs) - - -class AnnotatedLogger: - """Class that contains settings and the decorator method. - - Args: - ---- - annotations: Dictionary of annotations to be added to every log message - plugins: list of plugins to use - - Methods: - ------- - annotate_logs: Decorator that will insert the `annotated_logger` argument if - asked for in the method signature or let a provided AnnotatedAdapter to be - passed. Creates a new AnnotatedAdapter instance for each invocation of a - annotated function to isolate any annotations that are set during execution. - - """ - - def __init__( # noqa: PLR0913 - self, - annotations: dict[str, Any] | None = None, - plugins: list[BasePlugin] | None = None, - max_length: int | None = None, - log_level: int = logging.INFO, - name: str = "annotated_logger", - config: dict[str, Any] | Literal[False] | None = None, - ) -> None: - """Store the settings. - - Args: - ---- - annotations: Dictionary of static annotations - default None - plugins: List of plugins to be applied - default [BasePlugin] - is created and used - default None - max_length: Integer, maximum length of a message before it's broken into - multiple message and log calls. - default None - log_level: Integer, log level set for the shared root logger of the package. - - default logging.INFO (20) - name: Name of the shared root logger of the package. If more than one - `AnnotatedLogger` object is created in a project this should be set, - otherwise settings like level will be overwritten by the second to execute - - default 'annotated_logger' - config: Optional - logging config dictionary to be passed to - logging.config.dictConfig or False. If false dictConfig will not be called. - If not passed the DEFAULT_LOGGING_CONFIG will be used. A special - `annotated_filter` keyword is looked for, if present it will be - replaced with a `()` filter config to generate a filter for this - instance of `AnnotatedLogger`. - - """ - if plugins is None: - plugins = [] - - self.log_level = log_level - self.logger_root_name = name - self.logger_base = logging.getLogger(self.logger_root_name) - self.logger_base.setLevel(self.log_level) - self.annotations = annotations or {} - self.plugins = [BasePlugin()] - self.plugins.extend(plugins) - - if config is None: - config = deepcopy(DEFAULT_LOGGING_CONFIG) - if config: - for config_filter in config["filters"].values(): - if config_filter.get("annotated_filter"): - del config_filter["annotated_filter"] - config_filter["()"] = self.generate_filter - - # If we pass in config=False we don't want to configure. - # This is typically because we have another AnnotatedLogger - # object which did run the config and the dict config had config - # for both. - if config: - logging.config.dictConfig(config) - - self.max_length = max_length - - def _generate_logger( - self, - function: Function[S, P, R] | None = None, - cls: type | None = None, - logger_base_name: str | None = None, - ) -> AnnotatedAdapter: - """Generate a unique adapter with a unique logger object. - - This is required because the AnnotatedAdapter adds a filter to the logger. - The filter stores the annotations inside it, so they will mix if a new filter - and logger are not created each time. - """ - root_name = logger_base_name or self.logger_root_name - logger = logging.getLogger( - f"{root_name}.{uuid.uuid4()}" # pragma: no mutate - ) - - annotated_filter = self.generate_filter(function=function, cls=cls) - - return AnnotatedAdapter(logger, annotated_filter, self.max_length) - - def _action_annotation( - self, function: Function[S, P, R], key: str = "action" - ) -> dict[str, str]: - return {key: f"{function.__module__}:{function.__qualname__}"} - - def generate_filter( - self, - function: Function[S, P, R] | None = None, - cls: type[C_co] | None = None, - annotations: dict[str, Any] | None = None, - ) -> AnnotatedFilter: - """Create a AnnotatedFilter with the correct annotations and plugins.""" - annotations_passed = annotations - annotations = annotations or {} - if function: - annotations.update(self._action_annotation(function)) - class_annotations = {} - elif cls: - class_annotations = {"class": f"{cls.__module__}:{cls.__qualname__}"} - else: - class_annotations = {} - if not annotations_passed: - annotations.update(self.annotations) - - return AnnotatedFilter( - annotations=annotations, - class_annotations=class_annotations, - plugins=self.plugins, - ) - - #### Defaults - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_requested: Literal[False], - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - provided: Literal[False], - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[True], - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[True], - _typing_requested: Literal[False], - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - provided: Literal[False], - _typing_requested: Literal[False], - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[True], - provided: Literal[False], - ) -> NoInjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[True], - _typing_requested: Literal[False], - provided: Literal[False], - ) -> NoInjectionSelf[S, P, R]: ... - - #### Class True - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - _typing_class: Literal[True], - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S, P2] = None, - post_call: PostCall[S, P3] = None, - ) -> Callable[[type[C_co]], type[C_co]]: ... - - ### Instance False - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[False], - ) -> NoInjectionBare[P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[False], - _typing_requested: Literal[False], - ) -> NoInjectionBare[P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[False], - provided: Literal[False], - ) -> NoInjectionBare[P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[False], - _typing_requested: Literal[False], - provided: Literal[False], - ) -> NoInjectionBare[P, R]: ... - - ### Requested True - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_requested: Literal[True], - ) -> InjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[True], - _typing_requested: Literal[True], - ) -> InjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - provided: Literal[False], - _typing_requested: Literal[True], - ) -> InjectionSelf[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[True], - _typing_requested: Literal[True], - provided: Literal[False], - ) -> InjectionSelf[S, P, R]: ... - - ### Provided True, Requested True - # Can't provide it if it was not requested, - # so no overloads for not requested, but provided - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_requested: Literal[True], - provided: Literal[True], - ) -> InjectionSelfProvide[S, P, R]: ... - - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[True], - _typing_requested: Literal[True], - provided: Literal[True], - ) -> InjectionSelfProvide[S, P, R]: ... - - ### Instance False, Requested True - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - _typing_self: Literal[False], - _typing_requested: Literal[True], - ) -> InjectionBare[P, R]: ... - - ### Instance False, Requested True, Provided True - # Same not as above that you can't provide if not requested - @overload - def annotate_logs( - self, - logger_name: str | None = None, - *, - success_info: bool = True, # pragma: no mutate - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P2] = None, - _typing_self: Literal[False], - _typing_requested: Literal[True], - provided: Literal[True], - ) -> InjectionBareProvide[P, R]: ... - - # Between the overloads and the two inner method definitions, - # there's not much I can do to reduce the complexity more. - # So, ignoring the complexity metric - def annotate_logs( # noqa: C901 - self, - logger_name: str | None = None, - *, - success_info: bool = True, - pre_call: PreCall[S2, P2] = None, - post_call: PostCall[S2, P3] = None, - provided: bool = False, - _typing_self: bool = True, # pragma: no mutate - _typing_requested: bool = False, # pragma: no mutate - _typing_class: bool = False, # pragma: no mutate - ) -> Decorator[S, P, R] | Callable[[type[C_co]], type[C_co]]: - """Log start and end of function and provide an annotated logger if requested. - - Args: - ---- - logger_name: Optional - Specify the name of the logger attached to - the decorated function. - success_info: Log success at an info level, if falsey success will be - logged at debug. Default: True - provided: Boolean that indicates the caller will be providing it's - own annotated_logger. Default: False - pre_call: Method that takes the same arguments as the decorated function - and does something. Called before the function and the `start` log message. - post_call: Method that takes the same arguments as the decorated function - and does something. Called after the function and before the `success` - log message or in the exception handling. - _typing_self: Used only for type hint overloads. Indicates that the - decorated method is an instance method and has a self parameter. - Default: True - _typing_requested: Used only for type hint overloads. Indicates that the - decorated method is expecting an annotated_logger to be provided. - Default: False - - Notes: - ----- - In order to fully support type hinting, the annotated_logger argument - must be the first argument (after self/cls). Type hinting will only work - correctly if the _typing arguments are set correctly, but the code will - work fine at runtime without the _typing arguments. - - """ - - @overload - def decorator( - wrapped: SelfLoggerAndParams[S, P, R], - ) -> SelfAndParams[S, P, R] | SelfLoggerAndParams[S, P, R]: ... - - @overload - def decorator( - wrapped: LoggerAndParams[P, R], - ) -> ParamsOnly[P, R] | LoggerAndParams[P, R]: ... - - @overload - def decorator( - wrapped: SelfAndParams[S, P, R], - ) -> SelfAndParams[S, P, R]: ... - - @overload - def decorator( - wrapped: ParamsOnly[P, R], - ) -> ParamsOnly[P, R] | Empty[R]: ... - - @overload - def decorator(wrapped: type[C_co]) -> Callable[P, AnnotatedClass[C_co]]: ... - - def decorator( # noqa: C901 - wrapped: Function[S, P, R] | type[C_co], - ) -> Function[S, P, R] | Callable[P, AnnotatedClass[C_co]]: - if isinstance(wrapped, type): - - def wrap_class( - *args: P.args, **kwargs: P.kwargs - ) -> AnnotatedClass[C_co]: - logger = self._generate_logger( - cls=wrapped, logger_base_name=logger_name - ) - logger.debug("init") - new = cast(AnnotatedClass[C_co], wrapped(*args, **kwargs)) - new.annotated_logger = logger - return new - - return wrap_class - - (remove_args, inject_logger) = self._determine_signature_adjustments( - wrapped, provided=provided - ) - - @wraps( - wrapped, - remove_args=remove_args, - ) - def wrap_function(*args: P.args, **kwargs: P.kwargs) -> R: - __tracebackhide__ = True # pragma: no mutate - - post_call_attempted = False # pragma: no mutate - - new_args, new_kwargs, logger, pre_execution_annotations = inject_logger( - list(args), kwargs, logger_base_name=logger_name - ) - try: - start_time = time.perf_counter() - if pre_call: - pre_call(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue] - logger.debug("start") - - result = wrapped(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue] - logger.annotate(success=True) - if post_call: - post_call_attempted = True - _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue] - end_time = time.perf_counter() - logger.annotate(run_time=f"{end_time - start_time :.1f}") - with contextlib.suppress(TypeError): - logger.annotate(count=len(result)) # pyright: ignore[reportArgumentType] - - if success_info: - logger.info("success") - else: - logger.debug("success") - - # If we were provided with a logger object, set the annotations - # back to what they were before the wrapped method was called. - if pre_execution_annotations: - logger.filter.annotations = pre_execution_annotations - except Exception as e: - for plugin in logger.filter.plugins: - logger = plugin.uncaught_exception(e, logger) - logger.exception( - "Uncaught Exception in logged function", - ) - if post_call and not post_call_attempted: - _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue] - raise - return result - - return wrap_function - - return decorator - - def _determine_signature_adjustments( - self, - function: Function[S, P, R], - *, - provided: bool, - ) -> tuple[ - list[str], - Callable[ - Concatenate[list[Any], dict[str, Any], ...], - tuple[list[Any], dict[str, Any], AnnotatedAdapter, Annotations | None], - ], - ]: - written_signature = inspect.signature(function) - logger_requested = False # pragma: no mutate - remove_args = [] - index, instance_method = self._check_parameters_for_self_and_cls( - written_signature - ) - if "annotated_logger" in written_signature.parameters: - if list(written_signature.parameters.keys())[index] != "annotated_logger": - error_message = "annotated_logger must be the first argument" - raise TypeError(error_message) - - logger_requested = True - if not provided: - remove_args = ["annotated_logger"] - - def inject_logger( - args: list[Any], - kwargs: dict[str, Any], - logger_base_name: str | None = None, - ) -> tuple[list[Any], dict[str, Any], AnnotatedAdapter, Annotations | None]: - if not logger_requested: - logger = self._generate_logger( - function, logger_base_name=logger_base_name - ) - return (args, kwargs, logger, None) - - by_index = False # pragma: no mutate - # Check for a var positional or positional only - # If present that means we'll have values in args when invoking - # but, if not everything will be in kwargs - for v in written_signature.parameters.values(): - if v.kind == inspect.Parameter.VAR_POSITIONAL: - by_index = True - - new_args = copy(args) - new_kwargs = copy(kwargs) - if by_index: - logger, annotations, new_args = self._inject_by_index( - provided=provided, - instance_method=instance_method, - args=new_args, - index=index, - function=function, - logger_base_name=logger_base_name, - ) - else: - logger, annotations, new_kwargs = self._inject_by_kwarg( - provided=provided, - instance_method=instance_method, - kwargs=new_kwargs, - function=function, - logger_base_name=logger_base_name, - ) - - return new_args, new_kwargs, logger, annotations - - return remove_args, inject_logger - - def _inject_by_kwarg( - self, - *, - provided: bool, - instance_method: bool, - function: Function[S, P, R], - kwargs: dict[str, Any], - logger_base_name: str | None = None, - ) -> tuple[AnnotatedAdapter, Annotations | None, dict[str, Any]]: - if provided: - instance = kwargs["annotated_logger"] - elif instance_method: - instance = kwargs["self"] - else: - instance = False # pragma: no mutate - logger, annotations = self._pick_correct_logger( - function, instance, logger_base_name=logger_base_name - ) - if not provided: - kwargs["annotated_logger"] = logger - - return logger, annotations, kwargs - - def _inject_by_index( # noqa: PLR0913 - self, - *, - provided: bool, - instance_method: bool, - function: Function[S, P, R], - args: list[Any], - index: int, - logger_base_name: str | None = None, - ) -> tuple[AnnotatedAdapter, Annotations | None, list[Any]]: - if provided: - instance = args[index] - elif instance_method: - instance = args[0] - else: - instance = False # pragma: no mutate - logger, annotations = self._pick_correct_logger( - function, instance, logger_base_name=logger_base_name - ) - if not provided: - args.insert(index, logger) - return logger, annotations, args - - def _check_parameters_for_self_and_cls( - self, sig: inspect.Signature - ) -> tuple[int, bool]: - parameters = sig.parameters - index = 0 - instance_method = False - if "self" in parameters: - index = 1 - instance_method = True - if "cls" in parameters: - index = 1 - - return index, instance_method - - def _pick_correct_logger( - self, - function: Function[S, P, R], - instance: object | bool, - logger_base_name: str | None = None, - ) -> tuple[AnnotatedAdapter, Annotations | None]: - """Use the instance's logger and annotations if present.""" - if instance and hasattr(instance, "annotated_logger"): - logger = instance.annotated_logger # pyright: ignore[reportAttributeAccessIssue] - annotations = copy(logger.filter.annotations) - logger.filter.annotations.update(self._action_annotation(function)) - return (logger, annotations) - - if isinstance(instance, AnnotatedAdapter): - logger = instance - annotations = copy(logger.filter.annotations) - logger.filter.annotations.update( - self._action_annotation(function, key="subaction") - ) - return (logger, annotations) - - return ( - self._generate_logger(function, logger_base_name=logger_base_name), - None, - ) - - -def _attempt_post_call( - post_call: Callable[P, None], - logger: AnnotatedAdapter, - *args: P.args, - **kwargs: P.kwargs, -) -> None: - try: - if post_call: - post_call(*args, **kwargs) # pyright: ignore[reportCallIssue] - except Exception: - logger.annotate(success=False) - logger.exception("Post call failed") - raise diff --git a/annotated_logger/filter.py b/annotated_logger/filter.py deleted file mode 100644 index 02205c9..0000000 --- a/annotated_logger/filter.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -import logging -from copy import copy -from typing import Any - -import annotated_logger - -Annotations = dict[str, Any] - - -class AnnotatedFilter(logging.Filter): - """Filter class that stores the annotations and plugins.""" - - def __init__( - self, - annotations: Annotations | None = None, - class_annotations: Annotations | None = None, - plugins: list[annotated_logger.BasePlugin] | None = None, - ) -> None: - """Store the annotations, attributes and plugins.""" - self.annotations = annotations or {} - self.class_annotations = class_annotations or {} - self.plugins = plugins or [annotated_logger.BasePlugin()] - - # This allows plugins to determine what fields were added by the user - # vs the ones native to the log record - # TODO(crimsonknave): Make a test for this # noqa: TD003, FIX002 - self.base_attributes = logging.makeLogRecord({}).__dict__ # pragma: no mutate - - def _all_annotations(self) -> Annotations: - annotations = {} - annotations.update(copy(self.class_annotations)) - annotations.update(copy(self.annotations)) - annotations["annotated"] = True - return annotations - - def filter(self, record: logging.LogRecord) -> bool: - """Add the annotations to the record and allow plugins to filter the record. - - The `filter` method is called on each plugin in the order they are listed. - The plugin is then able to maniuplate the record object before the next plugin - sees it. Returning False from the filter method will stop the evaluation and - the log record won't be emitted. - """ - record.__dict__.update(self._all_annotations()) - for plugin in self.plugins: - try: - result = plugin.filter(record) - except Exception: # noqa: BLE001 - failed_plugins = record.__dict__.get("failed_plugins", []) - failed_plugins.append(str(plugin.__class__)) - record.__dict__["failed_plugins"] = failed_plugins - result = True - - if not result: - return False - return True diff --git a/annotated_logger/mocks.py b/annotated_logger/mocks.py deleted file mode 100644 index 232e0d6..0000000 --- a/annotated_logger/mocks.py +++ /dev/null @@ -1,253 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Any, Literal - -import pychoir -import pytest - - -class AssertLogged: - """Stores the data from a call to `assert_logged` and checks if there is a match.""" - - def __init__( - self, - level: str | pychoir.core.Matcher, - message: str | pychoir.core.Matcher, - present: dict[str, str], - absent: set[str] | Literal["ALL"], - *, - count: int | pychoir.core.Matcher, - ) -> None: - """Store the arguments that were passed to `assert_logged` and set defaults.""" - self.level = level - self.message = message - self.present = present - self.absent = absent - self.count = count - self.found = 0 - self.failed_matches: dict[str, int] = {} - - def check(self, mock: AnnotatedLogMock) -> None: - """Loop through calls in passed mock and check for matches.""" - for record in mock.records: - differences = self._check_record_matches(record) - if len(differences) == 0: - self.found = self.found + 1 - diff_str = str(differences) - if diff_str in self.failed_matches: - self.failed_matches[diff_str] += 1 - else: - self.failed_matches[diff_str] = 1 - - fail_message = self.build_message() - if len(fail_message) > 0: - pytest.fail("\n".join(fail_message)) - - def _failed_sort_key(self, failed_tuple: tuple[str, int]) -> str: - failed, count = failed_tuple - message_match = failed.count("Desired message") - count_diff = 0 # pragma: no mutate - if isinstance(self.count, int): - count_diff = abs(count - self.count) - number = ( - failed.count("Desired") - + failed.count("Missing key") - + failed.count("Unwanted key") - ) - length = len(failed) - # This will order by if the message matched then how the count differs - # then number of incorrect bits and finally the length - return f"{message_match}-{count_diff:04d}-{number:04d}-{length:04d}" # pragma: no mutate # noqa: E501 - - def build_message(self) -> list[str]: - """Create failure message.""" - if self.count == 0 and self.found == 0: - return [] - if self.found == 0: - fail_message = [ - f"No matching log record found. There were {sum(self.failed_matches.values())} log messages.", # noqa: E501 - ] - - fail_message.append("Desired:") - if isinstance(self.count, int): - fail_message.append(f"Count: {self.count}") - fail_message.append(f"Message: '{self.message}'") - fail_message.append(f"Level: '{self.level}'") - # only put in these if they were specified - fail_message.append(f"Present: '{self.present}'") - fail_message.append(f"Absent: '{self.absent}'") - fail_message.append("") - - if len(self.failed_matches) == 0: - return fail_message - fail_message.append( - "Below is a list of the values for the selected extras for those failed matches.", # noqa: E501 - ) - for match, count in sorted( - self.failed_matches.items(), key=self._failed_sort_key - ): - msg = match - if self.count and self.count != count: - msg = ( - match[:-1] - + f', "Desired {self.count} call{"" if self.count == 1 else "s"}, actual {count} call{"" if count == 1 else "s"}"' # noqa: E501 - + match[-1:] - ) - fail_message.append(msg) - return fail_message - - if self.count != self.found: - return [f"Found {self.found} matching messages, {self.count} were desired"] - return [] - - def _check_record_matches( - self, - record: logging.LogRecord, - ) -> list[str]: - differences = [] - # `levelname` is often renamed. But, `levelno` shouldn't be touched as often - # So, don't try to guess what the level name is, just use the levelno. - level = { - logging.DEBUG: "DEBUG", - logging.INFO: "INFO", - logging.WARNING: "WARNING", - logging.ERROR: "ERROR", - }[record.levelno] - actual = { - "level": level, - "msg": record.msg, - # The extras are already added as attributes, so this is the easiest way - # to get them. There are more things in here, but that should be fine - "extra": record.__dict__, - } - - if self.level != actual["level"]: - differences.append( - f"Desired level: {self.level}, actual level: {actual['level']}", - ) - # TODO @: Do a better string diff here # noqa: FIX002, TD003 - if self.message != actual["msg"]: - differences.append( - f"Desired message: '{self.message}', actual message: '{actual['msg']}'", - ) - - actual_keys = set(actual["extra"].keys()) - desired_keys = set(self.present.keys()) - - missing = desired_keys - actual_keys - unwanted = set() - if self.absent == AnnotatedLogMock.ALL: - unwanted = actual_keys - AnnotatedLogMock.DEFAULT_LOG_KEYS - elif isinstance(self.absent, set): - unwanted = actual_keys & self.absent - shared = desired_keys & actual_keys - differences.extend([f"Missing key: `{key}`" for key in sorted(missing)]) - - differences.extend([f"Unwanted key: `{key}`" for key in sorted(unwanted)]) - - differences.extend( - [ - f"Extra `{key}` value is incorrect. Desired `{self.present[key]}` ({self.present[key].__class__}) , actual `{actual['extra'][key]}` ({actual['extra'][key].__class__})" # noqa: E501 - for key in sorted(shared) - if self.present[key] != actual["extra"][key] - ] - ) - return differences - - -class AnnotatedLogMock(logging.Handler): - """Mock that captures logs and provides extra assertion logic.""" - - ALL = "ALL" - DEFAULT_LOG_KEYS = frozenset( - [ - "action", - "annotated", - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "lineno", - "message", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack_info", - "thread", - "threadName", - ] - ) - - def __init__(self, handler: logging.Handler) -> None: - """Store the handler and initialize the messages and records lists.""" - self.messages = [] - self.records = [] - self.handler = handler - - def __getattr__(self, name: str) -> Any: # noqa: ANN401 - """Fall back to the real handler object.""" - return getattr(self.handler, name) - - def handle(self, record: logging.LogRecord) -> bool: - """Wrap the real handle method, store the formatted message and log record.""" - self.messages.append(self.handler.format(record)) - self.records.append(record) - return self.handler.handle(record) - - def assert_logged( - self, - level: str | pychoir.core.Matcher | None = None, - message: str | pychoir.core.Matcher | None = None, - present: dict[str, Any] | None = None, - absent: str | set[str] | list[str] | None = None, - count: int | pychoir.core.Matcher | None = None, - ) -> None: - """Check if the mock received a log call that matches the arguments.""" - if level is None: - level = pychoir.existential.Anything() - elif isinstance(level, str): - level = level.upper() - if message is None: - message = pychoir.existential.Anything() - if present is None: - present = {} - if absent is None: - absent = [] - if isinstance(absent, list): - absent = set(absent) - if isinstance(absent, str) and absent != "ALL": - absent = {absent} - if count is None: - count = pychoir.numeric.IsPositive() - __tracebackhide__ = True # pragma: no mutate - assert_logged = AssertLogged(level, message, present, absent, count=count) - assert_logged.check(self) - - -@pytest.fixture -def annotated_logger_object() -> logging.Logger: - """Logger to wrap with the `annotated_logger_mock` fixture.""" - return logging.getLogger("annotated_logger") - - -@pytest.fixture -def annotated_logger_mock(annotated_logger_object: logging.Logger) -> AnnotatedLogMock: - """Fixture for a mock of the annotated logger.""" - handler = annotated_logger_object.handlers[0] - annotated_logger_object.removeHandler(handler) - mock_handler = AnnotatedLogMock( - handler=handler, - ) - - annotated_logger_object.addHandler(mock_handler) - return mock_handler diff --git a/annotated_logger/plugins.py b/annotated_logger/plugins.py deleted file mode 100644 index aaeec06..0000000 --- a/annotated_logger/plugins.py +++ /dev/null @@ -1,208 +0,0 @@ -from __future__ import annotations - -import contextlib -import logging -from typing import TYPE_CHECKING, Any, Callable - -from requests.exceptions import HTTPError - -from annotated_logger.filter import AnnotatedFilter - -if TYPE_CHECKING: # pragma: no cover - from annotated_logger import AnnotatedAdapter - - -class BasePlugin: - """Base class for plugins.""" - - def filter(self, _record: logging.LogRecord) -> bool: - """Determine if the record should be sent.""" - return True - - def uncaught_exception( - self, exception: Exception, logger: AnnotatedAdapter - ) -> AnnotatedAdapter: - """Handle an uncaught excaption.""" - if "success" not in logger.filter.annotations: - logger.annotate(success=False) - if "exception_title" not in logger.filter.annotations: - logger.annotate(exception_title=str(exception)) - return logger - - -class RuntimeAnnotationsPlugin(BasePlugin): - """Plugin that sets annotations dynamically.""" - - def __init__( - self, runtime_annotations: dict[str, Callable[[logging.LogRecord], Any]] - ) -> None: - """Store the runtime annotations.""" - self.runtime_annotations = runtime_annotations - - def filter(self, record: logging.LogRecord) -> bool: - """Add any configured runtime annotations.""" - for key, function in self.runtime_annotations.items(): - record.__dict__[key] = function(record) - return True - - -class RequestsPlugin(BasePlugin): - """Plugin for the requests library.""" - - def uncaught_exception( - self, exception: Exception, logger: AnnotatedAdapter - ) -> AnnotatedAdapter: - """Add the status code if possible.""" - if isinstance(exception, HTTPError) and exception.response is not None: - logger.annotate(status_code=exception.response.status_code) - logger.annotate(exception_title=exception.response.reason) - return logger - - -class RenamerPlugin(BasePlugin): - """Plugin that prevents name collisions.""" - - class FieldNotPresentError(Exception): - """Exception for a field that is supposed to be renamed, but is not present.""" - - def __init__(self, *, strict: bool = False, **kwargs: str) -> None: - """Store the list of names to rename and pre/post fixs.""" - self.targets = kwargs - self.strict = strict - - def filter(self, record: logging.LogRecord) -> bool: - """Adjust the name of any fields that match a provided list if they exist.""" - for new, old in self.targets.items(): - if old in record.__dict__: - record.__dict__[new] = record.__dict__[old] - del record.__dict__[old] - elif self.strict: - raise RenamerPlugin.FieldNotPresentError(old) - return True - - -class RemoverPlugin(BasePlugin): - """Plugin that removed fields.""" - - def __init__(self, targets: list[str] | str) -> None: - """Store the list of names to remove.""" - if isinstance(targets, str): - targets = [targets] - self.targets = targets - - def filter(self, record: logging.LogRecord) -> bool: - """Remove the specified fields.""" - for target in self.targets: - with contextlib.suppress(KeyError): - del record.__dict__[target] - return True - - -class NameAdjusterPlugin(BasePlugin): - """Plugin that prevents name collisions with splunk field names.""" - - def __init__(self, names: list[str], prefix: str = "", postfix: str = "") -> None: - """Store the list of names to rename and pre/post fixs.""" - self.names = names - self.prefix = prefix - self.postfix = postfix - - def filter(self, record: logging.LogRecord) -> bool: - """Adjust the name of any fields that match a provided list.""" - for name in self.names: - if name in record.__dict__: - value = record.__dict__[name] - del record.__dict__[name] - record.__dict__[f"{self.prefix}{name}{self.postfix}"] = value - return True - - -class NestedRemoverPlugin(BasePlugin): - """Plugin that removes nested fields.""" - - def __init__(self, keys_to_remove: list[str]) -> None: - """Store the list of keys to remove.""" - self.keys_to_remove = keys_to_remove - - def filter(self, record: logging.LogRecord) -> bool: - """Remove the specified fields.""" - - def delete_keys_nested( - target: dict, # pyright: ignore[reportMissingTypeArgument] - keys_to_remove: list, # pyright: ignore[reportMissingTypeArgument] - ) -> dict: # pyright: ignore[reportMissingTypeArgument] - for key in keys_to_remove: - with contextlib.suppress(KeyError): - del target[key] - for value in target.values(): - if isinstance(value, dict): - delete_keys_nested(value, keys_to_remove) - return target - - record.__dict__ = delete_keys_nested(record.__dict__, self.keys_to_remove) - return True - - -class GitHubActionsPlugin(BasePlugin): - """Plugin that will format log messages for actions annotations.""" - - def __init__(self, annotation_level: int) -> None: - """Save the annotation level.""" - self.annotation_level = annotation_level - self.base_attributes = logging.makeLogRecord({}).__dict__ # pragma: no mutate - self.attributes_to_exclude = {"annotated"} - - def filter(self, record: logging.LogRecord) -> bool: - """Set the actions command to be an annotation if desired.""" - if record.levelno < self.annotation_level: - return False - - added_attributes = { - k: v - for k, v in record.__dict__.items() - if k not in self.base_attributes and k not in self.attributes_to_exclude - } - record.added_attributes = added_attributes - name = record.levelname.lower() - if name == "info": # pragma: no cover - name = "notice" - record.github_annotation = f"{name}::" - - return True - - def logging_config(self) -> dict[str, dict[str, object]]: - """Generate the default logging config for the plugin.""" - return { - "handlers": { - "actions_handler": { - "class": "logging.StreamHandler", - "filters": ["actions_filter"], - "formatter": "actions_formatter", - }, - }, - "filters": { - "actions_filter": { - "()": AnnotatedFilter, - "plugins": [ - BasePlugin(), - self, - ], - }, - }, - "formatters": { - "actions_formatter": { - "format": "{github_annotation} {message} - {added_attributes}", - "style": "{", - }, - }, - "loggers": { - "annotated_logger.actions": { - "level": "DEBUG", - "handlers": [ - # This is from the default logging config - # "annotated_handler", - "actions_handler", - ], - }, - }, - } diff --git a/annotated_logger/py.typed b/annotated_logger/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/badge.svg b/badge.svg new file mode 100644 index 0000000..1b3eb31 --- /dev/null +++ b/badge.svg @@ -0,0 +1 @@ +Coverage: 100%Coverage100% \ No newline at end of file diff --git a/data.json b/data.json new file mode 100644 index 0000000..87f740d --- /dev/null +++ b/data.json @@ -0,0 +1 @@ +{"coverage": 100.0, "raw_data": {"meta": {"format": 3, "version": "7.9.1", "timestamp": "2025-06-30T19:30:25.575495", "branch_coverage": false, "show_contexts": false}, "files": {"annotated_logger/__init__.py": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24, 26, 27, 29, 37, 39, 40, 41, 42, 43, 44, 45, 46, 49, 50, 52, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 72, 73, 75, 85, 93, 96, 127, 128, 130, 140, 141, 142, 143, 144, 151, 153, 155, 156, 158, 160, 161, 162, 163, 164, 165, 166, 168, 169, 172, 173, 175, 182, 183, 184, 185, 189, 191, 200, 202, 214, 216, 224, 225, 227, 228, 229, 231, 232, 233, 234, 235, 236, 237, 238, 239, 245, 246, 247, 249, 251, 252, 254, 257, 258, 274, 306, 307, 309, 310, 311, 312, 313, 314, 315, 317, 318, 319, 320, 321, 322, 323, 329, 330, 332, 334, 346, 347, 351, 353, 355, 358, 360, 367, 368, 369, 370, 371, 372, 373, 375, 376, 377, 379, 386, 387, 396, 397, 407, 408, 418, 419, 429, 430, 439, 440, 451, 452, 463, 464, 475, 476, 489, 490, 501, 502, 512, 513, 524, 525, 536, 537, 550, 551, 561, 562, 573, 574, 585, 586, 601, 602, 613, 614, 627, 628, 641, 642, 657, 700, 701, 705, 706, 710, 711, 715, 716, 720, 721, 723, 726, 728, 731, 734, 735, 736, 737, 739, 741, 745, 749, 750, 752, 754, 757, 758, 759, 760, 761, 763, 764, 765, 766, 767, 768, 769, 770, 771, 773, 774, 776, 780, 781, 782, 783, 784, 785, 788, 789, 790, 791, 793, 795, 797, 809, 810, 811, 812, 815, 816, 817, 818, 820, 821, 822, 824, 829, 830, 833, 835, 839, 840, 841, 843, 844, 845, 846, 855, 863, 865, 867, 876, 877, 878, 879, 881, 882, 885, 886, 888, 890, 900, 901, 902, 903, 905, 906, 909, 910, 911, 913, 916, 917, 918, 919, 920, 921, 922, 923, 925, 927, 934, 935, 936, 937, 938, 940, 941, 942, 943, 946, 948, 954, 960, 961, 962, 963, 964, 965, 966], "summary": {"covered_lines": 314, "num_statements": 314, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2}, "missing_lines": [], "excluded_lines": [29, 30], "functions": {"AnnotatedIterator.__init__": {"executed_lines": [140, 141, 142, 143, 144, 151], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedIterator.__iter__": {"executed_lines": [155, 156], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedIterator.__next__": {"executed_lines": [160, 161, 162, 163, 164, 165, 166, 168, 169], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedAdapter.__init__": {"executed_lines": [182, 183, 184, 185, 189], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedAdapter.iterator": {"executed_lines": [200], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedAdapter.process": {"executed_lines": [214], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedAdapter.log": {"executed_lines": [224, 225, 227, 228, 229, 231, 232, 233, 234, 235, 236, 237, 238, 239, 245, 246, 247], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedAdapter.annotate": {"executed_lines": [251, 252, 254], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger.__init__": {"executed_lines": [306, 307, 309, 310, 311, 312, 313, 314, 315, 317, 318, 319, 320, 321, 322, 323, 329, 330, 332], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._generate_logger": {"executed_lines": [346, 347, 351, 353], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._action_annotation": {"executed_lines": [358], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger.generate_filter": {"executed_lines": [367, 368, 369, 370, 371, 372, 373, 375, 376, 377, 379], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger.annotate_logs": {"executed_lines": [700, 701, 705, 706, 710, 711, 715, 716, 720, 723, 795], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger.annotate_logs.decorator": {"executed_lines": [726, 728, 739, 741, 745, 749, 793], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger.annotate_logs.decorator.wrap_class": {"executed_lines": [731, 734, 735, 736, 737], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger.annotate_logs.decorator.wrap_function": {"executed_lines": [750, 752, 754, 757, 758, 759, 760, 761, 763, 764, 765, 766, 767, 768, 769, 770, 771, 773, 774, 776, 780, 781, 782, 783, 784, 785, 788, 789, 790, 791], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._determine_signature_adjustments": {"executed_lines": [809, 810, 811, 812, 815, 816, 817, 818, 820, 821, 822, 824, 865], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._determine_signature_adjustments.inject_logger": {"executed_lines": [829, 830, 833, 835, 839, 840, 841, 843, 844, 845, 846, 855, 863], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._inject_by_kwarg": {"executed_lines": [876, 877, 878, 879, 881, 882, 885, 886, 888], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._inject_by_index": {"executed_lines": [900, 901, 902, 903, 905, 906, 909, 910, 911], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._check_parameters_for_self_and_cls": {"executed_lines": [916, 917, 918, 919, 920, 921, 922, 923, 925], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger._pick_correct_logger": {"executed_lines": [934, 935, 936, 937, 938, 940, 941, 942, 943, 946, 948], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "_attempt_post_call": {"executed_lines": [960, 961, 962, 963, 964, 965, 966], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24, 26, 27, 29, 37, 39, 40, 41, 42, 43, 44, 45, 46, 49, 50, 52, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 72, 73, 75, 85, 93, 96, 127, 128, 130, 153, 158, 172, 173, 175, 191, 202, 216, 249, 257, 258, 274, 334, 355, 360, 386, 387, 396, 397, 407, 408, 418, 419, 429, 430, 439, 440, 451, 452, 463, 464, 475, 476, 489, 490, 501, 502, 512, 513, 524, 525, 536, 537, 550, 551, 561, 562, 573, 574, 585, 586, 601, 602, 613, 614, 627, 628, 641, 642, 657, 797, 867, 890, 913, 927, 954], "summary": {"covered_lines": 110, "num_statements": 110, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2}, "missing_lines": [], "excluded_lines": [29, 30]}}, "classes": {"AnnotatedClass": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedIterator": {"executed_lines": [140, 141, 142, 143, 144, 151, 155, 156, 160, 161, 162, 163, 164, 165, 166, 168, 169], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedAdapter": {"executed_lines": [182, 183, 184, 185, 189, 200, 214, 224, 225, 227, 228, 229, 231, 232, 233, 234, 235, 236, 237, 238, 239, 245, 246, 247, 251, 252, 254], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogger": {"executed_lines": [306, 307, 309, 310, 311, 312, 313, 314, 315, 317, 318, 319, 320, 321, 322, 323, 329, 330, 332, 346, 347, 351, 353, 358, 367, 368, 369, 370, 371, 372, 373, 375, 376, 377, 379, 700, 701, 705, 706, 710, 711, 715, 716, 720, 721, 723, 726, 728, 731, 734, 735, 736, 737, 739, 741, 745, 749, 750, 752, 754, 757, 758, 759, 760, 761, 763, 764, 765, 766, 767, 768, 769, 770, 771, 773, 774, 776, 780, 781, 782, 783, 784, 785, 788, 789, 790, 791, 793, 795, 809, 810, 811, 812, 815, 816, 817, 818, 820, 821, 822, 824, 829, 830, 833, 835, 839, 840, 841, 843, 844, 845, 846, 855, 863, 865, 876, 877, 878, 879, 881, 882, 885, 886, 888, 900, 901, 902, 903, 905, 906, 909, 910, 911, 916, 917, 918, 919, 920, 921, 922, 923, 925, 934, 935, 936, 937, 938, 940, 941, 942, 943, 946, 948], "summary": {"covered_lines": 153, "num_statements": 153, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24, 26, 27, 29, 37, 39, 40, 41, 42, 43, 44, 45, 46, 49, 50, 52, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 72, 73, 75, 85, 93, 96, 127, 128, 130, 153, 158, 172, 173, 175, 191, 202, 216, 249, 257, 258, 274, 334, 355, 360, 386, 387, 396, 397, 407, 408, 418, 419, 429, 430, 439, 440, 451, 452, 463, 464, 475, 476, 489, 490, 501, 502, 512, 513, 524, 525, 536, 537, 550, 551, 561, 562, 573, 574, 585, 586, 601, 602, 613, 614, 627, 628, 641, 642, 657, 797, 867, 890, 913, 927, 954, 960, 961, 962, 963, 964, 965, 966], "summary": {"covered_lines": 117, "num_statements": 117, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2}, "missing_lines": [], "excluded_lines": [29, 30]}}}, "annotated_logger/filter.py": {"executed_lines": [1, 3, 4, 5, 7, 9, 12, 13, 15, 22, 23, 24, 29, 31, 32, 33, 34, 35, 36, 38, 46, 47, 48, 49, 50, 51, 52, 53, 54, 56, 57, 58], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"AnnotatedFilter.__init__": {"executed_lines": [22, 23, 24, 29], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedFilter._all_annotations": {"executed_lines": [32, 33, 34, 35, 36], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedFilter.filter": {"executed_lines": [46, 47, 48, 49, 50, 51, 52, 53, 54, 56, 57, 58], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 9, 12, 13, 15, 31, 38], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"AnnotatedFilter": {"executed_lines": [22, 23, 24, 29, 32, 33, 34, 35, 36, 46, 47, 48, 49, 50, 51, 52, 53, 54, 56, 57, 58], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 9, 12, 13, 15, 31, 38], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "annotated_logger/mocks.py": {"executed_lines": [1, 3, 4, 6, 7, 10, 11, 13, 23, 24, 25, 26, 27, 28, 29, 31, 33, 34, 35, 36, 37, 38, 39, 41, 43, 44, 45, 47, 48, 49, 50, 51, 52, 53, 58, 61, 63, 65, 66, 67, 68, 72, 73, 74, 75, 76, 78, 79, 80, 82, 83, 84, 87, 90, 91, 92, 97, 98, 100, 101, 102, 104, 108, 111, 117, 125, 126, 130, 131, 135, 136, 138, 139, 140, 141, 142, 143, 144, 145, 147, 149, 156, 159, 160, 162, 163, 191, 193, 194, 195, 197, 199, 201, 203, 204, 205, 207, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 237, 238, 240, 243, 244, 246, 247, 248, 252, 253], "summary": {"covered_lines": 124, "num_statements": 124, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"AssertLogged.__init__": {"executed_lines": [23, 24, 25, 26, 27, 28, 29], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AssertLogged.check": {"executed_lines": [33, 34, 35, 36, 37, 38, 39, 41, 43, 44, 45], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AssertLogged._failed_sort_key": {"executed_lines": [48, 49, 50, 51, 52, 53, 58, 61], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AssertLogged.build_message": {"executed_lines": [65, 66, 67, 68, 72, 73, 74, 75, 76, 78, 79, 80, 82, 83, 84, 87, 90, 91, 92, 97, 98, 100, 101, 102], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AssertLogged._check_record_matches": {"executed_lines": [108, 111, 117, 125, 126, 130, 131, 135, 136, 138, 139, 140, 141, 142, 143, 144, 145, 147, 149, 156], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogMock.__init__": {"executed_lines": [193, 194, 195], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogMock.__getattr__": {"executed_lines": [199], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogMock.handle": {"executed_lines": [203, 204, 205], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogMock.assert_logged": {"executed_lines": [216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "annotated_logger_object": {"executed_lines": [240], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "annotated_logger_mock": {"executed_lines": [246, 247, 248, 252, 253], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 6, 7, 10, 11, 13, 31, 47, 63, 104, 159, 160, 162, 163, 191, 197, 201, 207, 237, 238, 243, 244], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"AssertLogged": {"executed_lines": [23, 24, 25, 26, 27, 28, 29, 33, 34, 35, 36, 37, 38, 39, 41, 43, 44, 45, 48, 49, 50, 51, 52, 53, 58, 61, 65, 66, 67, 68, 72, 73, 74, 75, 76, 78, 79, 80, 82, 83, 84, 87, 90, 91, 92, 97, 98, 100, 101, 102, 108, 111, 117, 125, 126, 130, 131, 135, 136, 138, 139, 140, 141, 142, 143, 144, 145, 147, 149, 156], "summary": {"covered_lines": 70, "num_statements": 70, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AnnotatedLogMock": {"executed_lines": [193, 194, 195, 199, 203, 204, 205, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 6, 7, 10, 11, 13, 31, 47, 63, 104, 159, 160, 162, 163, 191, 197, 201, 207, 237, 238, 240, 243, 244, 246, 247, 248, 252, 253], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "annotated_logger/plugins.py": {"executed_lines": [1, 3, 4, 5, 7, 9, 11, 15, 16, 18, 20, 22, 26, 27, 28, 29, 30, 33, 34, 36, 40, 42, 44, 45, 46, 49, 50, 52, 56, 57, 58, 59, 62, 63, 65, 66, 68, 70, 71, 73, 75, 76, 77, 78, 79, 80, 81, 84, 85, 87, 89, 90, 91, 93, 95, 96, 97, 98, 101, 102, 104, 106, 107, 108, 110, 112, 113, 114, 115, 116, 117, 120, 121, 123, 125, 127, 130, 134, 135, 136, 137, 138, 139, 140, 142, 143, 146, 147, 149, 151, 152, 153, 155, 157, 158, 160, 165, 166, 167, 168, 169, 171, 173, 175], "summary": {"covered_lines": 92, "num_statements": 92, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4}, "missing_lines": [], "excluded_lines": [11, 12, 167, 168], "functions": {"BasePlugin.filter": {"executed_lines": [20], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BasePlugin.uncaught_exception": {"executed_lines": [26, 27, 28, 29, 30], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RuntimeAnnotationsPlugin.__init__": {"executed_lines": [40], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RuntimeAnnotationsPlugin.filter": {"executed_lines": [44, 45, 46], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RequestsPlugin.uncaught_exception": {"executed_lines": [56, 57, 58, 59], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RenamerPlugin.__init__": {"executed_lines": [70, 71], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RenamerPlugin.filter": {"executed_lines": [75, 76, 77, 78, 79, 80, 81], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RemoverPlugin.__init__": {"executed_lines": [89, 90, 91], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RemoverPlugin.filter": {"executed_lines": [95, 96, 97, 98], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NameAdjusterPlugin.__init__": {"executed_lines": [106, 107, 108], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NameAdjusterPlugin.filter": {"executed_lines": [112, 113, 114, 115, 116, 117], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NestedRemoverPlugin.__init__": {"executed_lines": [125], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NestedRemoverPlugin.filter": {"executed_lines": [130, 142, 143], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NestedRemoverPlugin.filter.delete_keys_nested": {"executed_lines": [134, 135, 136, 137, 138, 139, 140], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GitHubActionsPlugin.__init__": {"executed_lines": [151, 152, 153], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GitHubActionsPlugin.filter": {"executed_lines": [157, 158, 160, 165, 166, 167, 168, 169, 171], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2}, "missing_lines": [], "excluded_lines": [167, 168]}, "GitHubActionsPlugin.logging_config": {"executed_lines": [175], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 9, 11, 15, 16, 18, 22, 33, 34, 36, 42, 49, 50, 52, 62, 63, 65, 66, 68, 73, 84, 85, 87, 93, 101, 102, 104, 110, 120, 121, 123, 127, 146, 147, 149, 155, 173], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2}, "missing_lines": [], "excluded_lines": [11, 12]}}, "classes": {"BasePlugin": {"executed_lines": [20, 26, 27, 28, 29, 30], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RuntimeAnnotationsPlugin": {"executed_lines": [40, 44, 45, 46], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RequestsPlugin": {"executed_lines": [56, 57, 58, 59], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RenamerPlugin": {"executed_lines": [70, 71, 75, 76, 77, 78, 79, 80, 81], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RenamerPlugin.FieldNotPresentError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RemoverPlugin": {"executed_lines": [89, 90, 91, 95, 96, 97, 98], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NameAdjusterPlugin": {"executed_lines": [106, 107, 108, 112, 113, 114, 115, 116, 117], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NestedRemoverPlugin": {"executed_lines": [125, 130, 134, 135, 136, 137, 138, 139, 140, 142, 143], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GitHubActionsPlugin": {"executed_lines": [151, 152, 153, 157, 158, 160, 165, 166, 167, 168, 169, 171, 175], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2}, "missing_lines": [], "excluded_lines": [167, 168]}, "": {"executed_lines": [1, 3, 4, 5, 7, 9, 11, 15, 16, 18, 22, 33, 34, 36, 42, 49, 50, 52, 62, 63, 65, 66, 68, 73, 84, 85, 87, 93, 101, 102, 104, 110, 120, 121, 123, 127, 146, 147, 149, 155, 173], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2}, "missing_lines": [], "excluded_lines": [11, 12]}}}, "example/__init__.py": {"executed_lines": [0], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "example/actions.py": {"executed_lines": [1, 2, 4, 9, 11, 13, 21, 22, 23, 24, 26, 34, 37, 38, 40, 41, 43, 45, 46, 48], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ActionsExample.first_step": {"executed_lines": [43], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ActionsExample.second_step": {"executed_lines": [48], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 2, 4, 9, 11, 13, 21, 22, 23, 24, 26, 34, 37, 38, 40, 41, 45, 46], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ActionsExample": {"executed_lines": [43, 48], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 2, 4, 9, 11, 13, 21, 22, 23, 24, 26, 34, 37, 38, 40, 41, 45, 46], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "example/api.py": {"executed_lines": [1, 3, 4, 6, 7, 9, 10, 13, 15, 18, 28, 31, 32, 33, 35, 37, 39, 40, 42, 43, 44, 45, 47, 48, 50, 51, 52, 53, 54, 56, 57, 59, 60, 61, 62, 64, 66, 68, 69, 70, 71], "summary": {"covered_lines": 40, "num_statements": 40, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"runtime": {"executed_lines": [15], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ApiClient.pre_call": {"executed_lines": [37], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ApiClient.check": {"executed_lines": [42, 43, 44, 45], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ApiClient.check_again": {"executed_lines": [50, 51, 52, 53, 54], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ApiClient.prepare": {"executed_lines": [59, 60, 61, 62], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ApiClient.throw_http_exception": {"executed_lines": [68, 69, 70, 71], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 6, 7, 9, 10, 13, 18, 28, 31, 32, 33, 35, 39, 40, 47, 48, 56, 57, 64, 66], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ApiClient": {"executed_lines": [37, 42, 43, 44, 45, 50, 51, 52, 53, 54, 59, 60, 61, 62, 68, 69, 70, 71], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 6, 7, 9, 10, 13, 15, 18, 28, 31, 32, 33, 35, 39, 40, 47, 48, 56, 57, 64, 66], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "example/calculator.py": {"executed_lines": [1, 3, 5, 6, 14, 15, 18, 20, 23, 39, 41, 44, 45, 51, 57, 58, 59, 61, 63, 64, 65, 66, 68, 75, 77, 84, 85, 86, 87, 88, 89, 90, 94, 100, 102, 106, 107, 108, 111, 112, 114, 120, 124, 126, 128, 129, 133, 134, 136, 138, 139, 143, 144, 145, 146, 147, 149, 150, 153, 155, 159, 162, 163, 164, 165, 167, 168, 170, 171, 173, 174, 176, 177, 178, 179, 180, 182, 183, 185, 187, 188, 190, 192, 193, 195, 196, 199, 200, 201, 202, 204, 205, 209, 212, 213, 214, 215, 217, 218, 219, 221, 222, 223, 225, 226, 227, 231, 232], "summary": {"covered_lines": 106, "num_statements": 106, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"runtime": {"executed_lines": [20], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.__init__": {"executed_lines": [57, 58, 59], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.check_zero_division": {"executed_lines": [63, 64, 65, 66], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.will_pass": {"executed_lines": [75], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.check_prediction_crashed_correctly": {"executed_lines": [84, 85, 86, 87, 88, 89, 90], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.divide": {"executed_lines": [102, 106, 107, 108, 111, 112], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.multiply": {"executed_lines": [124, 126], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.multiply2": {"executed_lines": [133, 134, 136], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.power": {"executed_lines": [143, 144, 145, 146, 147], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.add": {"executed_lines": [153, 155, 159, 162, 163, 164, 165], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.subtract": {"executed_lines": [170, 171], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.inverse": {"executed_lines": [176, 177, 178, 179, 180], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.pemdas_example": {"executed_lines": [185], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.is_odd": {"executed_lines": [190], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.factorial": {"executed_lines": [195, 196, 199, 200, 201, 202], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.sensitive_factorial": {"executed_lines": [209, 212, 213, 214, 215], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.is_math_cool": {"executed_lines": [221, 222, 223], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator.sanity_check": {"executed_lines": [231, 232], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 5, 6, 14, 15, 18, 23, 39, 41, 44, 45, 51, 61, 68, 77, 94, 100, 114, 120, 128, 129, 138, 139, 149, 150, 167, 168, 173, 174, 182, 183, 187, 188, 192, 193, 204, 205, 217, 218, 219, 225, 226, 227], "summary": {"covered_lines": 42, "num_statements": 42, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"BoomError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Calculator": {"executed_lines": [57, 58, 59, 63, 64, 65, 66, 75, 84, 85, 86, 87, 88, 89, 90, 102, 106, 107, 108, 111, 112, 124, 126, 133, 134, 136, 143, 144, 145, 146, 147, 153, 155, 159, 162, 163, 164, 165, 170, 171, 176, 177, 178, 179, 180, 185, 190, 195, 196, 199, 200, 201, 202, 209, 212, 213, 214, 215, 221, 222, 223, 231, 232], "summary": {"covered_lines": 63, "num_statements": 63, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 5, 6, 14, 15, 18, 20, 23, 39, 41, 44, 45, 51, 61, 68, 77, 94, 100, 114, 120, 128, 129, 138, 139, 149, 150, 167, 168, 173, 174, 182, 183, 187, 188, 192, 193, 204, 205, 217, 218, 219, 225, 226, 227], "summary": {"covered_lines": 43, "num_statements": 43, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "example/default.py": {"executed_lines": [1, 2, 6, 8, 11, 12, 14, 15, 17, 19, 20, 27, 28, 30, 31, 33, 34, 38, 40, 41, 43, 44, 48, 50, 51, 52, 53, 55, 56, 60, 61, 65, 66, 70, 72, 73, 74, 75, 77, 78, 87, 88, 89, 92, 93, 97, 98, 101, 102, 106, 108, 109, 110, 111], "summary": {"covered_lines": 53, "num_statements": 53, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"DefaultExample.foo": {"executed_lines": [17], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DefaultExample.var_args": {"executed_lines": [27, 28, 30, 31], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DefaultExample.var_kwargs": {"executed_lines": [38, 40, 41], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DefaultExample.var_args_and_kwargs": {"executed_lines": [48, 50, 51, 52, 53], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DefaultExample.var_args_and_kwargs_provided_outer": {"executed_lines": [60, 61], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DefaultExample.var_args_and_kwargs_provided": {"executed_lines": [70, 72, 73, 74, 75], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DefaultExample.positional_only": {"executed_lines": [87, 88, 89], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "var_args_and_kwargs_provided_outer": {"executed_lines": [97, 98], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "var_args_and_kwargs_provided": {"executed_lines": [106, 108, 109, 110, 111], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 2, 6, 8, 11, 12, 14, 15, 19, 20, 33, 34, 43, 44, 55, 56, 65, 66, 77, 78, 92, 93, 101, 102], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"DefaultExample": {"executed_lines": [17, 27, 28, 30, 31, 38, 40, 41, 48, 50, 51, 52, 53, 60, 61, 70, 72, 73, 74, 75, 87, 88, 89], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 2, 6, 8, 11, 12, 14, 15, 19, 20, 33, 34, 43, 44, 55, 56, 65, 66, 77, 78, 92, 93, 97, 98, 101, 102, 106, 108, 109, 110, 111], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "example/invalid_order.py": {"executed_lines": [1, 5, 7, 10, 11], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1}, "missing_lines": [], "excluded_lines": [13], "functions": {"wrong_order": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1}, "missing_lines": [], "excluded_lines": [13]}, "": {"executed_lines": [1, 5, 7, 10, 11], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [1, 5, 7, 10, 11], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1}, "missing_lines": [], "excluded_lines": [13]}}}, "example/logging_config.py": {"executed_lines": [1, 2, 4, 5, 35, 151, 153, 156, 171, 173, 180, 182, 183, 186, 188, 189, 190, 191, 194, 195, 197, 198, 199, 200, 203, 207, 209, 210, 211, 212, 215, 221, 223, 224, 225, 226, 227], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"runtime": {"executed_lines": [153], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "make_some_logs": {"executed_lines": [188, 189, 190, 191], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "make_some_annotated_logs": {"executed_lines": [197, 198, 199, 200], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "make_some_weird_logs": {"executed_lines": [209, 210, 211, 212], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "log_really_long_message": {"executed_lines": [223, 224, 225, 226, 227], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 2, 4, 5, 35, 151, 156, 171, 173, 180, 182, 183, 186, 194, 195, 203, 207, 215, 221], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [1, 2, 4, 5, 35, 151, 153, 156, 171, 173, 180, 182, 183, 186, 188, 189, 190, 191, 194, 195, 197, 198, 199, 200, 203, 207, 209, 210, 211, 212, 215, 221, 223, 224, 225, 226, 227], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}}, "totals": {"covered_lines": 821, "num_statements": 821, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 7}}, "coverage_path": "."} \ No newline at end of file diff --git a/endpoint.json b/endpoint.json new file mode 100644 index 0000000..2c66a68 --- /dev/null +++ b/endpoint.json @@ -0,0 +1 @@ +{"schemaVersion": 1, "label": "Coverage", "message": "100%", "color": "brightgreen"} \ No newline at end of file diff --git a/example/__init__.py b/example/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/example/actions.py b/example/actions.py deleted file mode 100644 index 9a581ee..0000000 --- a/example/actions.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -from copy import deepcopy - -from annotated_logger import ( - DEFAULT_LOGGING_CONFIG, - AnnotatedAdapter, - AnnotatedLogger, -) -from annotated_logger.plugins import GitHubActionsPlugin - -actions_plugin = GitHubActionsPlugin(annotation_level=logging.INFO) - -LOGGING = deepcopy(DEFAULT_LOGGING_CONFIG) - -# The GitHubActionsPlugin provides a `logging_config` method that returns some -# defaults that will annotate at the info (notice) and above. -# Making a copy of the default logging config and updating with this -# lets us keep the standard logger and also annotate in actions. -# But, we need to do it bit by bit so we are updating the loggers and so on -# instead of replacing the loggers. -LOGGING["loggers"].update(actions_plugin.logging_config()["loggers"]) -LOGGING["filters"].update(actions_plugin.logging_config()["filters"]) -LOGGING["handlers"].update(actions_plugin.logging_config()["handlers"]) -LOGGING["formatters"].update(actions_plugin.logging_config()["formatters"]) - -annotated_logger = AnnotatedLogger( - plugins=[ - actions_plugin, - ], - name="annotated_logger.actions", - config=LOGGING, -) - -annotate_logs = annotated_logger.annotate_logs - - -class ActionsExample: - """Example application that is designed to run in actions.""" - - @annotate_logs(_typing_requested=True) - def first_step(self, annotated_logger: AnnotatedAdapter) -> None: - """First step of your action.""" - annotated_logger.info("Step 1 running!") - - @annotate_logs(_typing_requested=True) - def second_step(self, annotated_logger: AnnotatedAdapter) -> None: - """Second step of your action.""" - annotated_logger.debug("Step 2 running!") diff --git a/example/api.py b/example/api.py deleted file mode 100644 index bb1c471..0000000 --- a/example/api.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Any - -from requests.exceptions import HTTPError -from requests.models import Response - -from annotated_logger import AnnotatedAdapter, AnnotatedLogger -from annotated_logger.plugins import RequestsPlugin, RuntimeAnnotationsPlugin - - -def runtime(_record: logging.LogRecord) -> str: - """Return the string every time.""" - return "this function is called every time" - - -annotated_logger = AnnotatedLogger( - annotations={"extra": "new data"}, - plugins=[ - RequestsPlugin(), - RuntimeAnnotationsPlugin({"runtime": runtime}), - ], - log_level=logging.DEBUG, - name="annotated_logger.api", -) - -annotate_logs = annotated_logger.annotate_logs - - -@annotate_logs(_typing_class=True) -class ApiClient: - """Example to test the RequestsPlugin.""" - - def pre_call(self, annotated_logger: AnnotatedAdapter) -> None: - """Add an annotation before the start message is logged.""" - annotated_logger.annotate(begin=True) - - @annotate_logs(_typing_requested=True, pre_call=pre_call) - def check(self, annotated_logger: AnnotatedAdapter) -> bool: - """Check if the request is good to send.""" - annotated_logger.annotate(valid=True) - annotated_logger.annotate(lasting="forever", persist=True) - annotated_logger.info("Check passed") - return True - - @annotate_logs(_typing_requested=True, pre_call=pre_call) - def check_again(self, annotated_logger: AnnotatedAdapter, *args: list[Any]) -> bool: - """Double check if the request is good to send.""" - annotated_logger.annotate(valid=True) - annotated_logger.annotate(lasting="forever", persist=True) - annotated_logger.annotate(args_length=len(args)) - annotated_logger.info("Check passed") - return True - - @annotate_logs(_typing_requested=True) - def prepare(self, annotated_logger: AnnotatedAdapter) -> bool: - """Prepare the request to send.""" - self.data = {} - annotated_logger.annotate(prepared=True) - annotated_logger.info("Preparation complete") - return True - - @annotate_logs() - # def throw_http_exception(self) -> None: - def throw_http_exception(self) -> None: - """Explode and log the status code.""" - response = Response() - response.status_code = 418 - response.reason = "i_am_a_teapot" - raise HTTPError(response=response, request=None) diff --git a/example/calculator.py b/example/calculator.py deleted file mode 100644 index bcca52e..0000000 --- a/example/calculator.py +++ /dev/null @@ -1,232 +0,0 @@ -from __future__ import annotations - -import logging - -from annotated_logger import AnnotatedAdapter, AnnotatedLogger -from annotated_logger.plugins import ( - NameAdjusterPlugin, - NestedRemoverPlugin, - RemoverPlugin, - RuntimeAnnotationsPlugin, -) - - -class BoomError(Exception): - """Boom.""" - - -def runtime(_record: logging.LogRecord) -> str: - """Return the string every time.""" - return "this function is called every time" - - -annotated_logger = AnnotatedLogger( - annotations={ - "extra": "new data", - "nested_extra": {"nested_key": {"double_nested_key": "value"}}, - }, - log_level=logging.DEBUG, - plugins=[ - NameAdjusterPlugin(names=["joke"], prefix="cheezy_"), - NameAdjusterPlugin(names=["power"], postfix="_overwhelming"), - RemoverPlugin("taskName"), - NestedRemoverPlugin(["double_nested_key"]), - RuntimeAnnotationsPlugin({"runtime": runtime}), - ], - name="annotated_logger.calculator", -) - -annotate_logs = annotated_logger.annotate_logs - -Number = int | float - - -class Calculator: - """Calculator application with very limited (and weird) functionality. - - This application is meant to highlight how to use the annotated-logger - package. It also serves as a way to test it. - """ - - def __init__(self, first: Number, second: Number) -> None: - """Create instance of example Calculator application. - - The Calculator is very simple and has only two attributes - that serve as two operands in a calculation. - """ - self.first = first - self.second = second - self.boom: bool = False - - def check_zero_division(self, annotated_logger: AnnotatedAdapter) -> None: - """Annotate if divide will crash.""" - will_crash = False - if self.second == 0: - will_crash = True - annotated_logger.annotate(will_crash=will_crash) - - def will_pass( - self, - annotated_logger: AnnotatedAdapter, - *args: ..., # noqa: ARG002 - **kwargs: ..., # noqa: ARG002 - ) -> None: - """Predict that the method will not crash.""" - annotated_logger.annotate(will_crash=False) - - def check_prediction_crashed_correctly( - self, - annotated_logger: AnnotatedAdapter, - *args: ..., # noqa: ARG002 - **kwargs: ..., # noqa: ARG002 - ) -> None: - """Check if the prediction was correct.""" - if self.boom: - annotated_logger.warning("boom") - raise BoomError - annotated_logger.annotate(first_again=self.first) - prediction = annotated_logger.filter.annotations.get("will_crash") - success = annotated_logger.filter.annotations["success"] - annotated_logger.info( - "Prediction result", extra={"result": success != prediction} - ) - - @annotated_logger.annotate_logs( - success_info=False, - pre_call=check_zero_division, - _typing_requested=True, - post_call=check_prediction_crashed_correctly, - ) - def divide(self, annotated_logger: AnnotatedAdapter) -> Number: - """Divide self.first by self.second.""" - annotated_logger.warning( - "If you divide by zero you'll create a singularity in the fabric of space-time!", # noqa: E501 - extra={"joke": True}, - ) - try: - return self.first / self.second - except ZeroDivisionError: - # This tests that calls to `logger.exception` work with sentry - # Normally you would only use `logger` outside of a logged function - annotated_logger.exception("This will get sent to sentry if enabled.") - raise - - @annotate_logs( - success_info=False, - _typing_requested=True, - pre_call=will_pass, - post_call=check_prediction_crashed_correctly, - ) - def multiply( - self, annotated_logger: AnnotatedAdapter, first: Number, second: Number - ) -> Number: - """Multiple the first parameter by the second parameter.""" - annotated_logger.annotate(first=first, second=second) - - return first * second - - @annotate_logs(success_info=False, provided=True, _typing_requested=True) - def multiply2( - self, annotated_logger: AnnotatedAdapter, first: Number, second: Number - ) -> Number: - """Multiple the first parameter by the second parameter.""" - annotated_logger.annotate(first=first) - annotated_logger.annotate(second=second) - - return first * second - - @annotate_logs(_typing_requested=True) - def power( - self, annotated_logger: AnnotatedAdapter, num: Number, power: int - ) -> Number: - """Raise num to the power power.""" - annotated_logger.annotate(power=True) - base: Number = num - for _ in range(1, power): - base = self.multiply2(annotated_logger, base, num) - return base - - @annotate_logs(success_info=False, _typing_requested=True) - def add(self, annotated_logger: AnnotatedAdapter) -> Number: - # def add(self, *args, annotated_logger: AnnotatedAdapter) -> Number: - """Add self.first and self.second.""" - annotated_logger.annotate(first=self.first, second=self.second, foo="bar") - - annotated_logger.info( - "This message will have 'other' as well as 'first' from the annotation above.", # noqa: E501 - extra={"other": "value"}, - ) - annotated_logger.info( - "This message will have the 'first' annotation and the defaults, but not the 'other'" # noqa: E501 - ) - if self.first is None: - annotated_logger.error("Must have a first value!") - self.first = 0 - return self.first + self.second - - @annotate_logs(_typing_requested=True) - def subtract(self, annotated_logger: AnnotatedAdapter) -> Number: - """Subtract the saved first from the saved second.""" - annotated_logger.debug("Order does matter when subtracting") - return self.first - self.second - - @annotate_logs(_typing_requested=True) - def inverse(self, annotated_logger: AnnotatedAdapter, num: Number) -> Number | bool: - """Divide 1 by num.""" - try: - return 1 / num - except ZeroDivisionError: - annotated_logger.exception("Cannot divide by zero!") - return False - - @annotate_logs() - def pemdas_example(self) -> list[int]: - """Check order of operations.""" - return [2 * 3 + 4, 2 * (3 + 4)] - - @annotate_logs(_typing_requested=False) - def is_odd(self, number: Number) -> bool: - """Check if number is odd.""" - return number % 2 == 0 - - @annotate_logs(_typing_requested=True) - def factorial(self, annotated_logger: AnnotatedAdapter, num: int) -> int: - """Perform the factiorial function.""" - annotated_logger.annotate(temp=True) - numbers = annotated_logger.iterator( - "factorial numbers", iter(range(1, num + 1)) - ) - total = 1 - for n in numbers: - total = total * n - return total - - @annotate_logs(_typing_requested=True) - def sensitive_factorial( - self, annotated_logger: AnnotatedAdapter, num: int, level: str = "info" - ) -> int: - """Perform the factorial function, but don't log the value.""" - numbers = annotated_logger.iterator( - "factorial numbers", iter(range(1, num + 1)), value=False, level=level - ) - total = 1 - for n in numbers: - total = total * n - return total - - @classmethod - @annotate_logs(_typing_requested=True) - def is_math_cool(cls: type[Calculator], annotated_logger: AnnotatedAdapter) -> bool: - """Answer the obvious question.""" - cls.sanity_check(annotated_logger, "is_math_cool") - annotated_logger.info("What a silly question!") - return True - - @classmethod - @annotate_logs(_typing_requested=True, provided=True) - def sanity_check( - cls: type[Calculator], annotated_logger: AnnotatedAdapter, source: str - ) -> None: - """Reassures the caller they are sane.""" - annotated_logger.annotate(sane=True) - annotated_logger.info("Checking sanity", extra={"source": source}) diff --git a/example/default.py b/example/default.py deleted file mode 100644 index 5876ee6..0000000 --- a/example/default.py +++ /dev/null @@ -1,111 +0,0 @@ -from annotated_logger import AnnotatedAdapter, AnnotatedLogger -from annotated_logger.plugins import RemoverPlugin - -# Actions runs in async.io it appears and that inejcts `taskName` -# But, locally that's not there, so it messes up the absent all tests -annotated_logger = AnnotatedLogger(plugins=[RemoverPlugin(["taskName"])]) - -annotate_logs = annotated_logger.annotate_logs - - -class DefaultExample: - """Simple example of the annotated logger with minimal config.""" - - @annotate_logs(_typing_requested=True) - def foo(self, annotated_logger: AnnotatedAdapter) -> None: - """Emit an info log.""" - annotated_logger.info("foo") - - @annotate_logs(_typing_requested=True) - def var_args( - self, - annotated_logger: AnnotatedAdapter, - _first: str, - *my_args: str, - ) -> bool: - """Take a splat of args.""" - annotated_logger.annotate(first=_first) - for i, arg in enumerate(my_args): - # Need to add persist=False to make the type checker happy - annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) - return True - - @annotate_logs(_typing_requested=True) - def var_kwargs( - self, annotated_logger: AnnotatedAdapter, _first: str, **kwargs: str - ) -> bool: - """Take a splat of args.""" - for name, arg in kwargs.items(): - # Need to add persist=False to make the type checker happy - annotated_logger.annotate(persist=False, **{name: arg}) - return True - - @annotate_logs(_typing_requested=True) - def var_args_and_kwargs( - self, annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str - ) -> bool: - """Take a splat of args.""" - for i, arg in enumerate(args): - # Need to add persist=False to make the type checker happy - annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) - for name, arg in kwargs.items(): - annotated_logger.annotate(persist=False, **{name: arg}) - return True - - @annotate_logs(_typing_requested=True) - def var_args_and_kwargs_provided_outer( - self, annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str - ) -> bool: - """Call the version that has the logger provided.""" - annotated_logger.annotate(outer=True) - return self.var_args_and_kwargs_provided( - annotated_logger, _first, *args, **kwargs - ) - - @annotate_logs(provided=True, _typing_requested=True) - def var_args_and_kwargs_provided( - self, annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str - ) -> bool: - """Take a splat of args.""" - for i, arg in enumerate(args): - # Need to add persist=False to make the type checker happy - annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) - for name, arg in kwargs.items(): - annotated_logger.annotate(persist=False, **{name: arg}) - return True - - @annotate_logs(_typing_requested=True) - def positional_only( - self, - annotated_logger: AnnotatedAdapter, - _first: str, - *, - _second: str, - # self, annotated_logger: AnnotatedAdapter, _first: str, *my_args: str - ) -> bool: - """Take a splat of args.""" - annotated_logger.annotate(first=_first) - annotated_logger.annotate(second=_second) - return True - - -@annotate_logs(_typing_self=False, _typing_requested=True) -def var_args_and_kwargs_provided_outer( - annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str -) -> bool: - """Call the version that has the logger provided.""" - annotated_logger.annotate(outer=True) - return var_args_and_kwargs_provided(annotated_logger, _first, *args, **kwargs) - - -@annotate_logs(provided=True, _typing_self=False, _typing_requested=True) -def var_args_and_kwargs_provided( - annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str -) -> bool: - """Take a splat of args.""" - for i, arg in enumerate(args): - # Need to add persist=False to make the type checker happy - annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) - for name, arg in kwargs.items(): - annotated_logger.annotate(persist=False, **{name: arg}) - return True diff --git a/example/invalid_order.py b/example/invalid_order.py deleted file mode 100644 index 0d487c9..0000000 --- a/example/invalid_order.py +++ /dev/null @@ -1,13 +0,0 @@ -from annotated_logger import AnnotatedAdapter, AnnotatedLogger - -# Actions runs in async.io it appears and that inejcts `taskName` -# But, locally that's not there, so it messes up the absent all tests -annotated_logger = AnnotatedLogger() - -annotate_logs = annotated_logger.annotate_logs - - -@annotate_logs(_typing_requested=True) -def wrong_order(_first: str, annotated_logger: AnnotatedAdapter) -> None: - """Blow up because we require annotated_logger be first.""" - annotated_logger.info("This should never be reachable.") # pragma: no cover diff --git a/example/logging_config.py b/example/logging_config.py deleted file mode 100644 index 3033226..0000000 --- a/example/logging_config.py +++ /dev/null @@ -1,227 +0,0 @@ -import logging -import logging.config - -from annotated_logger import AnnotatedAdapter, AnnotatedFilter, AnnotatedLogger -from annotated_logger.plugins import BasePlugin, RenamerPlugin, RuntimeAnnotationsPlugin - -# This logging config creates 4 loggers: -# * A logger for "annotated_logger.logging_config", which logs all messages as json and -# also logs errors as plain text. This is an example of how to log to multiple places. -# * A logger for "annotated_logger.logging_config_weird", which logs all messages at -# info and up. It has a different namespace (_weird instead of .weird) and so has -# isolated annotations. -# * A logger for "annotated_logger.logging_config.long", which logs all messages at info -# as text with a note added. This logger allows it's logs to propagate up and so the -# "annotated_logger.logging_config" logger will also log these messages in it's json -# format without the note from this logger. -# * A logger for "annotated_logger.logging_config.logger", which logs all messages as -# json. This logger does not propagate so that the "annotated_logger.logging_config" -# logger doesn't also log these messages. This logger is used by a non annotated -# method, but defines a filter that is annotated with the base annotations defined -# in `AnnotatedLogger(...`. This is an example of how to add annotations to external -# logs such as django. Note, the annotations this logger receives are based on the -# annotations passed in to the `AnnotatedLogger` invocation with the config, -# The second invocation for "weird" has different annotations. You should be able -# to have multiple of these with different annotations by invoking `AnnotatedLogger` -# multiple times and including `disable_existing_loggers` in the later configs. -# You can also provide custom annotations here if you wish to override the -# annotations from the annotated logger. -# -# Note: When creating multiple loggers, especially when doing so in different -# files/configs keep in mind that names should be unique or they will override -# eachother leaving you with a very confusing mess. -# If you want to see how to more easily merge settings into the default logging -# dict this package uses see the `actions.py` example. -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "filters": { - "logging_config.logger_filter": { - "annotated_filter": True, - # You can override the annotations that would be provided like so - # But, if you want to do that you are likely better off - # using a filter not associated with an AnnotatedLogger - # like the `logging_config.logger_filter_parens` below - "annotations": {"config_based_filter": True}, - }, - "logging_config.logger_filter_parens": { - "()": AnnotatedFilter, - "annotations": {"decorated": False, "class_based_filter": True}, - "class_annotations": {}, - "plugins": [ - BasePlugin(), - RuntimeAnnotationsPlugin({"custom_runtime": lambda _record: True}), - ], - }, - }, - "handlers": { - "logging_config.annotated_handler": { - "class": "logging.StreamHandler", - "formatter": "logging_config.annotated_formatter", - }, - "logging_config.logger_handler": { - "class": "logging.StreamHandler", - # Note that this handler is specifically attached to - # `annotated_logger.logging_config.logger` which we intend to use only as a - # logger not generated by a decorator. If you add filters to a handler that - # will be invoked when logging from a logger generated by the decorator - # AKA, logging via the `annotated_logger` parameter, these filters will - # be applied *after* the filters that are dynamically generated/updated - # and so can override any annotations that share a name. - # In this case, if a function annotated `custom_runtime` or one of the - # other fields set in the filters above, that value would be overridden - # by the value in the filter set in the config. - "filters": [ - "logging_config.logger_filter", - "logging_config.logger_filter_parens", - ], - "formatter": "logging_config.annotated_formatter", - }, - "logging_config.long_handler": { - "class": "logging.StreamHandler", - "formatter": "logging_config.long_formatter", - }, - "logging_config.error_handler": { - "class": "logging.StreamHandler", - "level": "ERROR", - "formatter": "logging_config.error_formatter", - }, - "logging_config.weird_handler": { - "class": "logging.StreamHandler", - "formatter": "logging_config.weird_formatter", - }, - }, - "formatters": { - "logging_config.annotated_formatter": { - "class": "pythonjsonlogger.json.JsonFormatter", - # Note that this format string uses `time` which is set by the renamer - # plugin. It also has `lvl` which is there strictly to test our fallback - # to using `levelno` in the mocks to determine level. - "format": "{time} {lvl} {name} {runtime} {message}", - "style": "{", - }, - "logging_config.error_formatter": { - "format": "{level} {message}", - "style": "{", - }, - "logging_config.long_formatter": { - "format": "{lvl} Long message, may be split {message}", - # 3.12 added support for defaults in dict configs - # With that we can add the format and defaults below - # for a more realistic example. Not all of the messages - # in the method we set to use this are long enough to be split, - # so, some of them don't have the message_part(s) fields. - # "format": "{level} {message_part}/{message_parts} {message}", # noqa: ERA001 E501 - # "defaults": {"message_part": 1, "message_parts": 1}, # noqa: ERA001 - "style": "{", - }, - "logging_config.weird_formatter": { - "class": "pythonjsonlogger.json.JsonFormatter", - "format": "{time} {lvl} {name} {message}", - "style": "{", - }, - }, - "loggers": { - "annotated_logger.logging_config": { - "level": "DEBUG", - "handlers": [ - "logging_config.annotated_handler", - "logging_config.error_handler", - ], - "propagate": True, - }, - "annotated_logger.logging_config_weird": { - "level": "INFO", - "handlers": ["logging_config.weird_handler"], - "propagate": True, - }, - "annotated_logger.logging_config.long": { - "level": "INFO", - "handlers": ["logging_config.long_handler"], - "propagate": True, - }, - "annotated_logger.logging_config.logger": { - "handlers": ["logging_config.logger_handler"], - "propagate": False, - }, - }, -} - - -def runtime(_record: logging.LogRecord) -> str: - """Return the string every time.""" - return "this function is called every time" - - -annotated_logger = AnnotatedLogger( - annotations={"hostname": "my-host"}, - # This is deprecated, use the RuntimeAnnotationsPlugin instead. - # This param is kept for backwards compatibility and creates a - # RuntimeAnnotationsPlugin instead. - # This is left as an example and to provide test coverage. - plugins=[ - RenamerPlugin(time="created", lvl="levelname"), - RuntimeAnnotationsPlugin({"runtime": runtime}), - ], - log_level=logging.DEBUG, - max_length=200, - name="annotated_logger.logging_config", - config=LOGGING, -) -annotate_logs = annotated_logger.annotate_logs - -weird_annotated_logger = AnnotatedLogger( - annotations={"weird": True}, - plugins=[RenamerPlugin(time="created", lvl="levelname")], - log_level=logging.INFO, - name="annotated_logger.logging_config_weird", - config=False, -) -weird_annotate_logs = weird_annotated_logger.annotate_logs - -logger = logging.getLogger("annotated_logger.logging_config.logger") -logger.setLevel("DEBUG") - - -def make_some_logs() -> None: - """Log messages using a native logging logger.""" - logger.debug("this is debug") - logger.info("this is info") - logger.warning("this is warning") - logger.error("this is error") - - -@annotate_logs(_typing_requested=True, _typing_self=False) -def make_some_annotated_logs(annotated_logger: AnnotatedAdapter) -> None: - """Log messages using the provided annotated_logger.""" - annotated_logger.debug("this is debug") - annotated_logger.info("this is info") - annotated_logger.warning("this is warning") - annotated_logger.error("this is error") - - -@weird_annotate_logs( - _typing_requested=True, - _typing_self=False, -) -def make_some_weird_logs(annotated_logger: AnnotatedAdapter) -> None: - """Log messages using the provided annotated_logger.""" - annotated_logger.debug("this is debug") - annotated_logger.info("this is info") - annotated_logger.warning("this is warning") - annotated_logger.error("this is error") - - -@annotate_logs( - _typing_requested=True, - _typing_self=False, - logger_name="annotated_logger.logging_config.long", - success_info=False, -) -def log_really_long_message(annotated_logger: AnnotatedAdapter) -> None: - """Log a message that is so long it will get split.""" - message = "1" * 200 + "2" * 200 + "3333" - annotated_logger.info(message) - annotated_logger.info("4" * 200) - annotated_logger.info("5" * 201) - annotated_logger.info("6" * 199) diff --git a/htmlcov/class_index.html b/htmlcov/class_index.html new file mode 100644 index 0000000..620cc8d --- /dev/null +++ b/htmlcov/class_index.html @@ -0,0 +1,332 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclassstatementsmissingexcludedcoverage
annotated_logger/__init__.pyAnnotatedIterator1700100%
annotated_logger/__init__.pyAnnotatedAdapter2700100%
annotated_logger/__init__.pyAnnotatedLogger15300100%
annotated_logger/__init__.py(no class)11702100%
annotated_logger/filter.pyAnnotatedFilter2100100%
annotated_logger/filter.py(no class)1000100%
annotated_logger/mocks.pyAssertLogged7000100%
annotated_logger/mocks.pyAnnotatedLogMock2600100%
annotated_logger/mocks.py(no class)2800100%
annotated_logger/plugins.pyBasePlugin600100%
annotated_logger/plugins.pyRuntimeAnnotationsPlugin400100%
annotated_logger/plugins.pyRequestsPlugin400100%
annotated_logger/plugins.pyRenamerPlugin900100%
annotated_logger/plugins.pyRemoverPlugin700100%
annotated_logger/plugins.pyNameAdjusterPlugin900100%
annotated_logger/plugins.pyNestedRemoverPlugin1100100%
annotated_logger/plugins.pyGitHubActionsPlugin1102100%
annotated_logger/plugins.py(no class)3102100%
example/actions.pyActionsExample200100%
example/actions.py(no class)1700100%
example/api.pyApiClient1800100%
example/api.py(no class)2200100%
example/calculator.pyCalculator6300100%
example/calculator.py(no class)4300100%
example/default.pyDefaultExample2300100%
example/default.py(no class)3000100%
example/invalid_order.py(no class)501100%
example/logging_config.py(no class)3700100%
Total 82107100%
+

+ No items found using the specified filter. +

+

3 empty classes skipped.

+
+ + + diff --git a/htmlcov/coverage_html_cb_6fb7b396.js b/htmlcov/coverage_html_cb_6fb7b396.js new file mode 100644 index 0000000..1face13 --- /dev/null +++ b/htmlcov/coverage_html_cb_6fb7b396.js @@ -0,0 +1,733 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + var child = cell.firstElementChild; + if (child.tagName === "A") { + child = child.firstElementChild; + } + if (child instanceof HTMLDataElement && child.value) { + return child.value; + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB; + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction. + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + var direction; + if (currentSortOrder === "none") { + direction = th.dataset.defaultSortOrder || "ascending"; + } + else if (currentSortOrder === "ascending") { + direction = "descending"; + } + else { + direction = "ascending"; + } + th.setAttribute("aria-sort", direction); + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM. + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (direction === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr)); + + // Save the sort order for next time. + if (th.id !== "region") { + let th_id = "file"; // Sort by file if we don't have a column id + let current_direction = direction; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)) + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + "th_id": th.id, + "direction": current_direction + })); + if (th.id !== th_id || document.getElementById("region")) { + // Sort column has changed, unset sorting by function or class. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": false, + "region_direction": current_direction + })); + } + } + else { + // Sort column has changed to by function or class, remember that. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": true, + "region_direction": direction + })); + } +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + // Observe filter keyevents. + const filter_handler = (event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = new Array(table.rows[0].cells.length).fill(0); + // Accumulate the percentage as fraction + totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection + + var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); + const casefold = (text === text.toLowerCase()); + const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); + + // Hide / show elements. + table_body_rows.forEach(row => { + var show = false; + // Check the text filter. + for (let column = 0; column < totals.length; column++) { + cell = row.cells[column]; + if (cell.classList.contains("name")) { + var celltext = cell.textContent; + if (casefold) { + celltext = celltext.toLowerCase(); + } + if (celltext.includes(text)) { + show = true; + } + } + } + + // Check the "hide covered" filter. + if (show && hide100) { + const [numer, denom] = row.cells[row.cells.length - 1].dataset.ratio.split(" "); + show = (numer !== denom); + } + + if (!show) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 0; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.classList.contains("name")) { + continue; + } + if (column === totals.length - 1) { + // Last column contains percentage + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } + else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + const footer = table.tFoot.rows[0]; + // Calculate new dynamic sum values based on visible rows. + for (let column = 0; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + if (cell.classList.contains("name")) { + continue; + } + + // Set value into dynamic footer cell element. + if (column === totals.length - 1) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } + else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + }); + + document.getElementById("filter").addEventListener("input", debounce(filter_handler)); + document.getElementById("hide100").addEventListener("input", debounce(filter_handler)); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); + document.getElementById("hide100").dispatchEvent(new Event("input")); +}; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; + +// Set up the click-to-sort columns. +coverage.wire_up_sorting = function () { + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + let th_id = "file", direction = "ascending"; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)); + } + let by_region = false, region_direction = "ascending"; + const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION); + if (sorted_by_region) { + ({ + by_region, + region_direction + } = JSON.parse(sorted_by_region)); + } + + const region_id = "region"; + if (by_region && document.getElementById(region_id)) { + direction = region_direction; + } + // If we are in a page that has a column with id of "region", sort on + // it if the last sort was by function or class. + let th; + if (document.getElementById(region_id)) { + th = document.getElementById(by_region ? region_id : th_id); + } + else { + th = document.getElementById(th_id); + } + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; +coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + coverage.wire_up_sorting(); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } + else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } + else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } + else { + coverage.pyfile_ready(); + } +}); diff --git a/htmlcov/favicon_32_cb_58284776.png b/htmlcov/favicon_32_cb_58284776.png new file mode 100644 index 0000000..8649f04 Binary files /dev/null and b/htmlcov/favicon_32_cb_58284776.png differ diff --git a/htmlcov/function_index.html b/htmlcov/function_index.html new file mode 100644 index 0000000..cb417ce --- /dev/null +++ b/htmlcov/function_index.html @@ -0,0 +1,948 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunctionstatementsmissingexcludedcoverage
annotated_logger/__init__.pyAnnotatedIterator.__init__600100%
annotated_logger/__init__.pyAnnotatedIterator.__iter__200100%
annotated_logger/__init__.pyAnnotatedIterator.__next__900100%
annotated_logger/__init__.pyAnnotatedAdapter.__init__500100%
annotated_logger/__init__.pyAnnotatedAdapter.iterator100100%
annotated_logger/__init__.pyAnnotatedAdapter.process100100%
annotated_logger/__init__.pyAnnotatedAdapter.log1700100%
annotated_logger/__init__.pyAnnotatedAdapter.annotate300100%
annotated_logger/__init__.pyAnnotatedLogger.__init__1900100%
annotated_logger/__init__.pyAnnotatedLogger._generate_logger400100%
annotated_logger/__init__.pyAnnotatedLogger._action_annotation100100%
annotated_logger/__init__.pyAnnotatedLogger.generate_filter1100100%
annotated_logger/__init__.pyAnnotatedLogger.annotate_logs1100100%
annotated_logger/__init__.pyAnnotatedLogger.annotate_logs.decorator100100%
annotated_logger/__init__.pyAnnotatedLogger.annotate_logs.decorator700100%
annotated_logger/__init__.pyAnnotatedLogger.annotate_logs.decorator.wrap_class500100%
annotated_logger/__init__.pyAnnotatedLogger.annotate_logs.decorator.wrap_function3000100%
annotated_logger/__init__.pyAnnotatedLogger._determine_signature_adjustments1300100%
annotated_logger/__init__.pyAnnotatedLogger._determine_signature_adjustments.inject_logger1300100%
annotated_logger/__init__.pyAnnotatedLogger._inject_by_kwarg900100%
annotated_logger/__init__.pyAnnotatedLogger._inject_by_index900100%
annotated_logger/__init__.pyAnnotatedLogger._check_parameters_for_self_and_cls900100%
annotated_logger/__init__.pyAnnotatedLogger._pick_correct_logger1100100%
annotated_logger/__init__.py_attempt_post_call700100%
annotated_logger/__init__.py(no function)11002100%
annotated_logger/filter.pyAnnotatedFilter.__init__400100%
annotated_logger/filter.pyAnnotatedFilter._all_annotations500100%
annotated_logger/filter.pyAnnotatedFilter.filter1200100%
annotated_logger/filter.py(no function)1000100%
annotated_logger/mocks.pyAssertLogged.__init__700100%
annotated_logger/mocks.pyAssertLogged.check1100100%
annotated_logger/mocks.pyAssertLogged._failed_sort_key800100%
annotated_logger/mocks.pyAssertLogged.build_message2400100%
annotated_logger/mocks.pyAssertLogged._check_record_matches2000100%
annotated_logger/mocks.pyAnnotatedLogMock.__init__300100%
annotated_logger/mocks.pyAnnotatedLogMock.__getattr__100100%
annotated_logger/mocks.pyAnnotatedLogMock.handle300100%
annotated_logger/mocks.pyAnnotatedLogMock.assert_logged1900100%
annotated_logger/mocks.pyannotated_logger_object100100%
annotated_logger/mocks.pyannotated_logger_mock500100%
annotated_logger/mocks.py(no function)2200100%
annotated_logger/plugins.pyBasePlugin.filter100100%
annotated_logger/plugins.pyBasePlugin.uncaught_exception500100%
annotated_logger/plugins.pyRuntimeAnnotationsPlugin.__init__100100%
annotated_logger/plugins.pyRuntimeAnnotationsPlugin.filter300100%
annotated_logger/plugins.pyRequestsPlugin.uncaught_exception400100%
annotated_logger/plugins.pyRenamerPlugin.__init__200100%
annotated_logger/plugins.pyRenamerPlugin.filter700100%
annotated_logger/plugins.pyRemoverPlugin.__init__300100%
annotated_logger/plugins.pyRemoverPlugin.filter400100%
annotated_logger/plugins.pyNameAdjusterPlugin.__init__300100%
annotated_logger/plugins.pyNameAdjusterPlugin.filter600100%
annotated_logger/plugins.pyNestedRemoverPlugin.__init__100100%
annotated_logger/plugins.pyNestedRemoverPlugin.filter300100%
annotated_logger/plugins.pyNestedRemoverPlugin.filter.delete_keys_nested700100%
annotated_logger/plugins.pyGitHubActionsPlugin.__init__300100%
annotated_logger/plugins.pyGitHubActionsPlugin.filter702100%
annotated_logger/plugins.pyGitHubActionsPlugin.logging_config100100%
annotated_logger/plugins.py(no function)3102100%
example/actions.pyActionsExample.first_step100100%
example/actions.pyActionsExample.second_step100100%
example/actions.py(no function)1700100%
example/api.pyruntime100100%
example/api.pyApiClient.pre_call100100%
example/api.pyApiClient.check400100%
example/api.pyApiClient.check_again500100%
example/api.pyApiClient.prepare400100%
example/api.pyApiClient.throw_http_exception400100%
example/api.py(no function)2100100%
example/calculator.pyruntime100100%
example/calculator.pyCalculator.__init__300100%
example/calculator.pyCalculator.check_zero_division400100%
example/calculator.pyCalculator.will_pass100100%
example/calculator.pyCalculator.check_prediction_crashed_correctly700100%
example/calculator.pyCalculator.divide600100%
example/calculator.pyCalculator.multiply200100%
example/calculator.pyCalculator.multiply2300100%
example/calculator.pyCalculator.power500100%
example/calculator.pyCalculator.add700100%
example/calculator.pyCalculator.subtract200100%
example/calculator.pyCalculator.inverse500100%
example/calculator.pyCalculator.pemdas_example100100%
example/calculator.pyCalculator.is_odd100100%
example/calculator.pyCalculator.factorial600100%
example/calculator.pyCalculator.sensitive_factorial500100%
example/calculator.pyCalculator.is_math_cool300100%
example/calculator.pyCalculator.sanity_check200100%
example/calculator.py(no function)4200100%
example/default.pyDefaultExample.foo100100%
example/default.pyDefaultExample.var_args400100%
example/default.pyDefaultExample.var_kwargs300100%
example/default.pyDefaultExample.var_args_and_kwargs500100%
example/default.pyDefaultExample.var_args_and_kwargs_provided_outer200100%
example/default.pyDefaultExample.var_args_and_kwargs_provided500100%
example/default.pyDefaultExample.positional_only300100%
example/default.pyvar_args_and_kwargs_provided_outer200100%
example/default.pyvar_args_and_kwargs_provided500100%
example/default.py(no function)2300100%
example/invalid_order.py(no function)500100%
example/logging_config.pyruntime100100%
example/logging_config.pymake_some_logs400100%
example/logging_config.pymake_some_annotated_logs400100%
example/logging_config.pymake_some_weird_logs400100%
example/logging_config.pylog_really_long_message500100%
example/logging_config.py(no function)1900100%
Total 82107100%
+

+ No items found using the specified filter. +

+

27 empty functions skipped.

+
+ + + diff --git a/htmlcov/index.html b/htmlcov/index.html new file mode 100644 index 0000000..21cde51 --- /dev/null +++ b/htmlcov/index.html @@ -0,0 +1,175 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filestatementsmissingexcludedcoverage
annotated_logger/__init__.py31402100%
annotated_logger/filter.py3100100%
annotated_logger/mocks.py12400100%
annotated_logger/plugins.py9204100%
example/actions.py1900100%
example/api.py4000100%
example/calculator.py10600100%
example/default.py5300100%
example/invalid_order.py501100%
example/logging_config.py3700100%
Total82107100%
+

+ No items found using the specified filter. +

+

1 empty file skipped.

+
+ + + diff --git a/htmlcov/keybd_closed_cb_ce680311.png b/htmlcov/keybd_closed_cb_ce680311.png new file mode 100644 index 0000000..ba119c4 Binary files /dev/null and b/htmlcov/keybd_closed_cb_ce680311.png differ diff --git a/htmlcov/status.json b/htmlcov/status.json new file mode 100644 index 0000000..d337fcf --- /dev/null +++ b/htmlcov/status.json @@ -0,0 +1 @@ +{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.9.1","globals":"c53f9aa0015a39e0fe4b119446104c87","files":{"z_beb44c9891d1179a___init___py":{"hash":"2645ccd7e388b059dc4290de40e21c41","index":{"url":"z_beb44c9891d1179a___init___py.html","file":"annotated_logger/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":314,"n_excluded":2,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_beb44c9891d1179a_filter_py":{"hash":"ae4581fe8fb9820d695a0a072e1bd7e7","index":{"url":"z_beb44c9891d1179a_filter_py.html","file":"annotated_logger/filter.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":31,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_beb44c9891d1179a_mocks_py":{"hash":"63faafd014a204ecc4c45315c6ee4068","index":{"url":"z_beb44c9891d1179a_mocks_py.html","file":"annotated_logger/mocks.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":124,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_beb44c9891d1179a_plugins_py":{"hash":"68b4a191b73aa1750cd0efb07ca33a3b","index":{"url":"z_beb44c9891d1179a_plugins_py.html","file":"annotated_logger/plugins.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":92,"n_excluded":4,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_70983d692f648185_actions_py":{"hash":"ee78484470c2ab71a7d0c76ad7a4b32b","index":{"url":"z_70983d692f648185_actions_py.html","file":"example/actions.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":19,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_70983d692f648185_api_py":{"hash":"25c161f830b58193a5a297360225b232","index":{"url":"z_70983d692f648185_api_py.html","file":"example/api.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":40,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_70983d692f648185_calculator_py":{"hash":"057fbded3fbcb00f546b8cee98f25cdd","index":{"url":"z_70983d692f648185_calculator_py.html","file":"example/calculator.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":106,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_70983d692f648185_default_py":{"hash":"25a5154e668e5b49904630edae1e271d","index":{"url":"z_70983d692f648185_default_py.html","file":"example/default.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":53,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_70983d692f648185_invalid_order_py":{"hash":"d6782cd5f5a0d4a82739941542eab04a","index":{"url":"z_70983d692f648185_invalid_order_py.html","file":"example/invalid_order.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":5,"n_excluded":1,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_70983d692f648185_logging_config_py":{"hash":"fa9b307aa118d1ce0e1090be378700c6","index":{"url":"z_70983d692f648185_logging_config_py.html","file":"example/logging_config.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":37,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}} \ No newline at end of file diff --git a/htmlcov/style_cb_81f8c14c.css b/htmlcov/style_cb_81f8c14c.css new file mode 100644 index 0000000..e54e87a --- /dev/null +++ b/htmlcov/style_cb_81f8c14c.css @@ -0,0 +1,337 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } } + +@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } } + +header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; } + +#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } } + +#filter_container #filter:focus { border-color: #007acc; } + +#filter_container :disabled ~ label { color: #ccc; } + +@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } } + +#filter_container label { font-size: .875em; color: #666; } + +@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { background: #333; } } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str, #source p .t .fst { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; } + +#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"] .arrows::after { content: " ▲"; } + +#index th[aria-sort="descending"] .arrows::after { content: " ▼"; } + +#index td.name { font-size: 1.15em; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index td.name .no-noun { font-style: italic; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } + +#index tr.region:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } } + +#index tr.region:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/htmlcov/z_70983d692f648185_actions_py.html b/htmlcov/z_70983d692f648185_actions_py.html new file mode 100644 index 0000000..04c6f1e --- /dev/null +++ b/htmlcov/z_70983d692f648185_actions_py.html @@ -0,0 +1,145 @@ + + + + + Coverage for example/actions.py: 100% + + + + + +
+
+

+ Coverage for example/actions.py: + 100% +

+ +

+ 19 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1import logging 

+

2from copy import deepcopy 

+

3 

+

4from annotated_logger import ( 

+

5 DEFAULT_LOGGING_CONFIG, 

+

6 AnnotatedAdapter, 

+

7 AnnotatedLogger, 

+

8) 

+

9from annotated_logger.plugins import GitHubActionsPlugin 

+

10 

+

11actions_plugin = GitHubActionsPlugin(annotation_level=logging.INFO) 

+

12 

+

13LOGGING = deepcopy(DEFAULT_LOGGING_CONFIG) 

+

14 

+

15# The GitHubActionsPlugin provides a `logging_config` method that returns some 

+

16# defaults that will annotate at the info (notice) and above. 

+

17# Making a copy of the default logging config and updating with this 

+

18# lets us keep the standard logger and also annotate in actions. 

+

19# But, we need to do it bit by bit so we are updating the loggers and so on 

+

20# instead of replacing the loggers. 

+

21LOGGING["loggers"].update(actions_plugin.logging_config()["loggers"]) 

+

22LOGGING["filters"].update(actions_plugin.logging_config()["filters"]) 

+

23LOGGING["handlers"].update(actions_plugin.logging_config()["handlers"]) 

+

24LOGGING["formatters"].update(actions_plugin.logging_config()["formatters"]) 

+

25 

+

26annotated_logger = AnnotatedLogger( 

+

27 plugins=[ 

+

28 actions_plugin, 

+

29 ], 

+

30 name="annotated_logger.actions", 

+

31 config=LOGGING, 

+

32) 

+

33 

+

34annotate_logs = annotated_logger.annotate_logs 

+

35 

+

36 

+

37class ActionsExample: 

+

38 """Example application that is designed to run in actions.""" 

+

39 

+

40 @annotate_logs(_typing_requested=True) 

+

41 def first_step(self, annotated_logger: AnnotatedAdapter) -> None: 

+

42 """First step of your action.""" 

+

43 annotated_logger.info("Step 1 running!") 

+

44 

+

45 @annotate_logs(_typing_requested=True) 

+

46 def second_step(self, annotated_logger: AnnotatedAdapter) -> None: 

+

47 """Second step of your action.""" 

+

48 annotated_logger.debug("Step 2 running!") 

+
+ + + diff --git a/htmlcov/z_70983d692f648185_api_py.html b/htmlcov/z_70983d692f648185_api_py.html new file mode 100644 index 0000000..374f95f --- /dev/null +++ b/htmlcov/z_70983d692f648185_api_py.html @@ -0,0 +1,168 @@ + + + + + Coverage for example/api.py: 100% + + + + + +
+
+

+ Coverage for example/api.py: + 100% +

+ +

+ 40 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2 

+

3import logging 

+

4from typing import Any 

+

5 

+

6from requests.exceptions import HTTPError 

+

7from requests.models import Response 

+

8 

+

9from annotated_logger import AnnotatedAdapter, AnnotatedLogger 

+

10from annotated_logger.plugins import RequestsPlugin, RuntimeAnnotationsPlugin 

+

11 

+

12 

+

13def runtime(_record: logging.LogRecord) -> str: 

+

14 """Return the string every time.""" 

+

15 return "this function is called every time" 

+

16 

+

17 

+

18annotated_logger = AnnotatedLogger( 

+

19 annotations={"extra": "new data"}, 

+

20 plugins=[ 

+

21 RequestsPlugin(), 

+

22 RuntimeAnnotationsPlugin({"runtime": runtime}), 

+

23 ], 

+

24 log_level=logging.DEBUG, 

+

25 name="annotated_logger.api", 

+

26) 

+

27 

+

28annotate_logs = annotated_logger.annotate_logs 

+

29 

+

30 

+

31@annotate_logs(_typing_class=True) 

+

32class ApiClient: 

+

33 """Example to test the RequestsPlugin.""" 

+

34 

+

35 def pre_call(self, annotated_logger: AnnotatedAdapter) -> None: 

+

36 """Add an annotation before the start message is logged.""" 

+

37 annotated_logger.annotate(begin=True) 

+

38 

+

39 @annotate_logs(_typing_requested=True, pre_call=pre_call) 

+

40 def check(self, annotated_logger: AnnotatedAdapter) -> bool: 

+

41 """Check if the request is good to send.""" 

+

42 annotated_logger.annotate(valid=True) 

+

43 annotated_logger.annotate(lasting="forever", persist=True) 

+

44 annotated_logger.info("Check passed") 

+

45 return True 

+

46 

+

47 @annotate_logs(_typing_requested=True, pre_call=pre_call) 

+

48 def check_again(self, annotated_logger: AnnotatedAdapter, *args: list[Any]) -> bool: 

+

49 """Double check if the request is good to send.""" 

+

50 annotated_logger.annotate(valid=True) 

+

51 annotated_logger.annotate(lasting="forever", persist=True) 

+

52 annotated_logger.annotate(args_length=len(args)) 

+

53 annotated_logger.info("Check passed") 

+

54 return True 

+

55 

+

56 @annotate_logs(_typing_requested=True) 

+

57 def prepare(self, annotated_logger: AnnotatedAdapter) -> bool: 

+

58 """Prepare the request to send.""" 

+

59 self.data = {} 

+

60 annotated_logger.annotate(prepared=True) 

+

61 annotated_logger.info("Preparation complete") 

+

62 return True 

+

63 

+

64 @annotate_logs() 

+

65 # def throw_http_exception(self) -> None: 

+

66 def throw_http_exception(self) -> None: 

+

67 """Explode and log the status code.""" 

+

68 response = Response() 

+

69 response.status_code = 418 

+

70 response.reason = "i_am_a_teapot" 

+

71 raise HTTPError(response=response, request=None) 

+
+ + + diff --git a/htmlcov/z_70983d692f648185_calculator_py.html b/htmlcov/z_70983d692f648185_calculator_py.html new file mode 100644 index 0000000..c60ed22 --- /dev/null +++ b/htmlcov/z_70983d692f648185_calculator_py.html @@ -0,0 +1,329 @@ + + + + + Coverage for example/calculator.py: 100% + + + + + +
+
+

+ Coverage for example/calculator.py: + 100% +

+ +

+ 106 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2 

+

3import logging 

+

4 

+

5from annotated_logger import AnnotatedAdapter, AnnotatedLogger 

+

6from annotated_logger.plugins import ( 

+

7 NameAdjusterPlugin, 

+

8 NestedRemoverPlugin, 

+

9 RemoverPlugin, 

+

10 RuntimeAnnotationsPlugin, 

+

11) 

+

12 

+

13 

+

14class BoomError(Exception): 

+

15 """Boom.""" 

+

16 

+

17 

+

18def runtime(_record: logging.LogRecord) -> str: 

+

19 """Return the string every time.""" 

+

20 return "this function is called every time" 

+

21 

+

22 

+

23annotated_logger = AnnotatedLogger( 

+

24 annotations={ 

+

25 "extra": "new data", 

+

26 "nested_extra": {"nested_key": {"double_nested_key": "value"}}, 

+

27 }, 

+

28 log_level=logging.DEBUG, 

+

29 plugins=[ 

+

30 NameAdjusterPlugin(names=["joke"], prefix="cheezy_"), 

+

31 NameAdjusterPlugin(names=["power"], postfix="_overwhelming"), 

+

32 RemoverPlugin("taskName"), 

+

33 NestedRemoverPlugin(["double_nested_key"]), 

+

34 RuntimeAnnotationsPlugin({"runtime": runtime}), 

+

35 ], 

+

36 name="annotated_logger.calculator", 

+

37) 

+

38 

+

39annotate_logs = annotated_logger.annotate_logs 

+

40 

+

41Number = int | float 

+

42 

+

43 

+

44class Calculator: 

+

45 """Calculator application with very limited (and weird) functionality. 

+

46 

+

47 This application is meant to highlight how to use the annotated-logger 

+

48 package. It also serves as a way to test it. 

+

49 """ 

+

50 

+

51 def __init__(self, first: Number, second: Number) -> None: 

+

52 """Create instance of example Calculator application. 

+

53 

+

54 The Calculator is very simple and has only two attributes 

+

55 that serve as two operands in a calculation. 

+

56 """ 

+

57 self.first = first 

+

58 self.second = second 

+

59 self.boom: bool = False 

+

60 

+

61 def check_zero_division(self, annotated_logger: AnnotatedAdapter) -> None: 

+

62 """Annotate if divide will crash.""" 

+

63 will_crash = False 

+

64 if self.second == 0: 

+

65 will_crash = True 

+

66 annotated_logger.annotate(will_crash=will_crash) 

+

67 

+

68 def will_pass( 

+

69 self, 

+

70 annotated_logger: AnnotatedAdapter, 

+

71 *args: ..., # noqa: ARG002 

+

72 **kwargs: ..., # noqa: ARG002 

+

73 ) -> None: 

+

74 """Predict that the method will not crash.""" 

+

75 annotated_logger.annotate(will_crash=False) 

+

76 

+

77 def check_prediction_crashed_correctly( 

+

78 self, 

+

79 annotated_logger: AnnotatedAdapter, 

+

80 *args: ..., # noqa: ARG002 

+

81 **kwargs: ..., # noqa: ARG002 

+

82 ) -> None: 

+

83 """Check if the prediction was correct.""" 

+

84 if self.boom: 

+

85 annotated_logger.warning("boom") 

+

86 raise BoomError 

+

87 annotated_logger.annotate(first_again=self.first) 

+

88 prediction = annotated_logger.filter.annotations.get("will_crash") 

+

89 success = annotated_logger.filter.annotations["success"] 

+

90 annotated_logger.info( 

+

91 "Prediction result", extra={"result": success != prediction} 

+

92 ) 

+

93 

+

94 @annotated_logger.annotate_logs( 

+

95 success_info=False, 

+

96 pre_call=check_zero_division, 

+

97 _typing_requested=True, 

+

98 post_call=check_prediction_crashed_correctly, 

+

99 ) 

+

100 def divide(self, annotated_logger: AnnotatedAdapter) -> Number: 

+

101 """Divide self.first by self.second.""" 

+

102 annotated_logger.warning( 

+

103 "If you divide by zero you'll create a singularity in the fabric of space-time!", # noqa: E501 

+

104 extra={"joke": True}, 

+

105 ) 

+

106 try: 

+

107 return self.first / self.second 

+

108 except ZeroDivisionError: 

+

109 # This tests that calls to `logger.exception` work with sentry 

+

110 # Normally you would only use `logger` outside of a logged function 

+

111 annotated_logger.exception("This will get sent to sentry if enabled.") 

+

112 raise 

+

113 

+

114 @annotate_logs( 

+

115 success_info=False, 

+

116 _typing_requested=True, 

+

117 pre_call=will_pass, 

+

118 post_call=check_prediction_crashed_correctly, 

+

119 ) 

+

120 def multiply( 

+

121 self, annotated_logger: AnnotatedAdapter, first: Number, second: Number 

+

122 ) -> Number: 

+

123 """Multiple the first parameter by the second parameter.""" 

+

124 annotated_logger.annotate(first=first, second=second) 

+

125 

+

126 return first * second 

+

127 

+

128 @annotate_logs(success_info=False, provided=True, _typing_requested=True) 

+

129 def multiply2( 

+

130 self, annotated_logger: AnnotatedAdapter, first: Number, second: Number 

+

131 ) -> Number: 

+

132 """Multiple the first parameter by the second parameter.""" 

+

133 annotated_logger.annotate(first=first) 

+

134 annotated_logger.annotate(second=second) 

+

135 

+

136 return first * second 

+

137 

+

138 @annotate_logs(_typing_requested=True) 

+

139 def power( 

+

140 self, annotated_logger: AnnotatedAdapter, num: Number, power: int 

+

141 ) -> Number: 

+

142 """Raise num to the power power.""" 

+

143 annotated_logger.annotate(power=True) 

+

144 base: Number = num 

+

145 for _ in range(1, power): 

+

146 base = self.multiply2(annotated_logger, base, num) 

+

147 return base 

+

148 

+

149 @annotate_logs(success_info=False, _typing_requested=True) 

+

150 def add(self, annotated_logger: AnnotatedAdapter) -> Number: 

+

151 # def add(self, *args, annotated_logger: AnnotatedAdapter) -> Number: 

+

152 """Add self.first and self.second.""" 

+

153 annotated_logger.annotate(first=self.first, second=self.second, foo="bar") 

+

154 

+

155 annotated_logger.info( 

+

156 "This message will have 'other' as well as 'first' from the annotation above.", # noqa: E501 

+

157 extra={"other": "value"}, 

+

158 ) 

+

159 annotated_logger.info( 

+

160 "This message will have the 'first' annotation and the defaults, but not the 'other'" # noqa: E501 

+

161 ) 

+

162 if self.first is None: 

+

163 annotated_logger.error("Must have a first value!") 

+

164 self.first = 0 

+

165 return self.first + self.second 

+

166 

+

167 @annotate_logs(_typing_requested=True) 

+

168 def subtract(self, annotated_logger: AnnotatedAdapter) -> Number: 

+

169 """Subtract the saved first from the saved second.""" 

+

170 annotated_logger.debug("Order does matter when subtracting") 

+

171 return self.first - self.second 

+

172 

+

173 @annotate_logs(_typing_requested=True) 

+

174 def inverse(self, annotated_logger: AnnotatedAdapter, num: Number) -> Number | bool: 

+

175 """Divide 1 by num.""" 

+

176 try: 

+

177 return 1 / num 

+

178 except ZeroDivisionError: 

+

179 annotated_logger.exception("Cannot divide by zero!") 

+

180 return False 

+

181 

+

182 @annotate_logs() 

+

183 def pemdas_example(self) -> list[int]: 

+

184 """Check order of operations.""" 

+

185 return [2 * 3 + 4, 2 * (3 + 4)] 

+

186 

+

187 @annotate_logs(_typing_requested=False) 

+

188 def is_odd(self, number: Number) -> bool: 

+

189 """Check if number is odd.""" 

+

190 return number % 2 == 0 

+

191 

+

192 @annotate_logs(_typing_requested=True) 

+

193 def factorial(self, annotated_logger: AnnotatedAdapter, num: int) -> int: 

+

194 """Perform the factiorial function.""" 

+

195 annotated_logger.annotate(temp=True) 

+

196 numbers = annotated_logger.iterator( 

+

197 "factorial numbers", iter(range(1, num + 1)) 

+

198 ) 

+

199 total = 1 

+

200 for n in numbers: 

+

201 total = total * n 

+

202 return total 

+

203 

+

204 @annotate_logs(_typing_requested=True) 

+

205 def sensitive_factorial( 

+

206 self, annotated_logger: AnnotatedAdapter, num: int, level: str = "info" 

+

207 ) -> int: 

+

208 """Perform the factorial function, but don't log the value.""" 

+

209 numbers = annotated_logger.iterator( 

+

210 "factorial numbers", iter(range(1, num + 1)), value=False, level=level 

+

211 ) 

+

212 total = 1 

+

213 for n in numbers: 

+

214 total = total * n 

+

215 return total 

+

216 

+

217 @classmethod 

+

218 @annotate_logs(_typing_requested=True) 

+

219 def is_math_cool(cls: type[Calculator], annotated_logger: AnnotatedAdapter) -> bool: 

+

220 """Answer the obvious question.""" 

+

221 cls.sanity_check(annotated_logger, "is_math_cool") 

+

222 annotated_logger.info("What a silly question!") 

+

223 return True 

+

224 

+

225 @classmethod 

+

226 @annotate_logs(_typing_requested=True, provided=True) 

+

227 def sanity_check( 

+

228 cls: type[Calculator], annotated_logger: AnnotatedAdapter, source: str 

+

229 ) -> None: 

+

230 """Reassures the caller they are sane.""" 

+

231 annotated_logger.annotate(sane=True) 

+

232 annotated_logger.info("Checking sanity", extra={"source": source}) 

+
+ + + diff --git a/htmlcov/z_70983d692f648185_default_py.html b/htmlcov/z_70983d692f648185_default_py.html new file mode 100644 index 0000000..e6ca7d1 --- /dev/null +++ b/htmlcov/z_70983d692f648185_default_py.html @@ -0,0 +1,208 @@ + + + + + Coverage for example/default.py: 100% + + + + + +
+
+

+ Coverage for example/default.py: + 100% +

+ +

+ 53 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from annotated_logger import AnnotatedAdapter, AnnotatedLogger 

+

2from annotated_logger.plugins import RemoverPlugin 

+

3 

+

4# Actions runs in async.io it appears and that inejcts `taskName` 

+

5# But, locally that's not there, so it messes up the absent all tests 

+

6annotated_logger = AnnotatedLogger(plugins=[RemoverPlugin(["taskName"])]) 

+

7 

+

8annotate_logs = annotated_logger.annotate_logs 

+

9 

+

10 

+

11class DefaultExample: 

+

12 """Simple example of the annotated logger with minimal config.""" 

+

13 

+

14 @annotate_logs(_typing_requested=True) 

+

15 def foo(self, annotated_logger: AnnotatedAdapter) -> None: 

+

16 """Emit an info log.""" 

+

17 annotated_logger.info("foo") 

+

18 

+

19 @annotate_logs(_typing_requested=True) 

+

20 def var_args( 

+

21 self, 

+

22 annotated_logger: AnnotatedAdapter, 

+

23 _first: str, 

+

24 *my_args: str, 

+

25 ) -> bool: 

+

26 """Take a splat of args.""" 

+

27 annotated_logger.annotate(first=_first) 

+

28 for i, arg in enumerate(my_args): 

+

29 # Need to add persist=False to make the type checker happy 

+

30 annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) 

+

31 return True 

+

32 

+

33 @annotate_logs(_typing_requested=True) 

+

34 def var_kwargs( 

+

35 self, annotated_logger: AnnotatedAdapter, _first: str, **kwargs: str 

+

36 ) -> bool: 

+

37 """Take a splat of args.""" 

+

38 for name, arg in kwargs.items(): 

+

39 # Need to add persist=False to make the type checker happy 

+

40 annotated_logger.annotate(persist=False, **{name: arg}) 

+

41 return True 

+

42 

+

43 @annotate_logs(_typing_requested=True) 

+

44 def var_args_and_kwargs( 

+

45 self, annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str 

+

46 ) -> bool: 

+

47 """Take a splat of args.""" 

+

48 for i, arg in enumerate(args): 

+

49 # Need to add persist=False to make the type checker happy 

+

50 annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) 

+

51 for name, arg in kwargs.items(): 

+

52 annotated_logger.annotate(persist=False, **{name: arg}) 

+

53 return True 

+

54 

+

55 @annotate_logs(_typing_requested=True) 

+

56 def var_args_and_kwargs_provided_outer( 

+

57 self, annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str 

+

58 ) -> bool: 

+

59 """Call the version that has the logger provided.""" 

+

60 annotated_logger.annotate(outer=True) 

+

61 return self.var_args_and_kwargs_provided( 

+

62 annotated_logger, _first, *args, **kwargs 

+

63 ) 

+

64 

+

65 @annotate_logs(provided=True, _typing_requested=True) 

+

66 def var_args_and_kwargs_provided( 

+

67 self, annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str 

+

68 ) -> bool: 

+

69 """Take a splat of args.""" 

+

70 for i, arg in enumerate(args): 

+

71 # Need to add persist=False to make the type checker happy 

+

72 annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) 

+

73 for name, arg in kwargs.items(): 

+

74 annotated_logger.annotate(persist=False, **{name: arg}) 

+

75 return True 

+

76 

+

77 @annotate_logs(_typing_requested=True) 

+

78 def positional_only( 

+

79 self, 

+

80 annotated_logger: AnnotatedAdapter, 

+

81 _first: str, 

+

82 *, 

+

83 _second: str, 

+

84 # self, annotated_logger: AnnotatedAdapter, _first: str, *my_args: str 

+

85 ) -> bool: 

+

86 """Take a splat of args.""" 

+

87 annotated_logger.annotate(first=_first) 

+

88 annotated_logger.annotate(second=_second) 

+

89 return True 

+

90 

+

91 

+

92@annotate_logs(_typing_self=False, _typing_requested=True) 

+

93def var_args_and_kwargs_provided_outer( 

+

94 annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str 

+

95) -> bool: 

+

96 """Call the version that has the logger provided.""" 

+

97 annotated_logger.annotate(outer=True) 

+

98 return var_args_and_kwargs_provided(annotated_logger, _first, *args, **kwargs) 

+

99 

+

100 

+

101@annotate_logs(provided=True, _typing_self=False, _typing_requested=True) 

+

102def var_args_and_kwargs_provided( 

+

103 annotated_logger: AnnotatedAdapter, _first: str, *args: str, **kwargs: str 

+

104) -> bool: 

+

105 """Take a splat of args.""" 

+

106 for i, arg in enumerate(args): 

+

107 # Need to add persist=False to make the type checker happy 

+

108 annotated_logger.annotate(persist=False, **{f"arg{i}": arg}) 

+

109 for name, arg in kwargs.items(): 

+

110 annotated_logger.annotate(persist=False, **{name: arg}) 

+

111 return True 

+
+ + + diff --git a/htmlcov/z_70983d692f648185_invalid_order_py.html b/htmlcov/z_70983d692f648185_invalid_order_py.html new file mode 100644 index 0000000..106b927 --- /dev/null +++ b/htmlcov/z_70983d692f648185_invalid_order_py.html @@ -0,0 +1,110 @@ + + + + + Coverage for example/invalid_order.py: 100% + + + + + +
+
+

+ Coverage for example/invalid_order.py: + 100% +

+ +

+ 5 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from annotated_logger import AnnotatedAdapter, AnnotatedLogger 

+

2 

+

3# Actions runs in async.io it appears and that inejcts `taskName` 

+

4# But, locally that's not there, so it messes up the absent all tests 

+

5annotated_logger = AnnotatedLogger() 

+

6 

+

7annotate_logs = annotated_logger.annotate_logs 

+

8 

+

9 

+

10@annotate_logs(_typing_requested=True) 

+

11def wrong_order(_first: str, annotated_logger: AnnotatedAdapter) -> None: 

+

12 """Blow up because we require annotated_logger be first.""" 

+

13 annotated_logger.info("This should never be reachable.") # pragma: no cover 

+
+ + + diff --git a/htmlcov/z_70983d692f648185_logging_config_py.html b/htmlcov/z_70983d692f648185_logging_config_py.html new file mode 100644 index 0000000..fb21620 --- /dev/null +++ b/htmlcov/z_70983d692f648185_logging_config_py.html @@ -0,0 +1,324 @@ + + + + + Coverage for example/logging_config.py: 100% + + + + + +
+
+

+ Coverage for example/logging_config.py: + 100% +

+ +

+ 37 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1import logging 

+

2import logging.config 

+

3 

+

4from annotated_logger import AnnotatedAdapter, AnnotatedFilter, AnnotatedLogger 

+

5from annotated_logger.plugins import BasePlugin, RenamerPlugin, RuntimeAnnotationsPlugin 

+

6 

+

7# This logging config creates 4 loggers: 

+

8# * A logger for "annotated_logger.logging_config", which logs all messages as json and 

+

9# also logs errors as plain text. This is an example of how to log to multiple places. 

+

10# * A logger for "annotated_logger.logging_config_weird", which logs all messages at 

+

11# info and up. It has a different namespace (_weird instead of .weird) and so has 

+

12# isolated annotations. 

+

13# * A logger for "annotated_logger.logging_config.long", which logs all messages at info 

+

14# as text with a note added. This logger allows it's logs to propagate up and so the 

+

15# "annotated_logger.logging_config" logger will also log these messages in it's json 

+

16# format without the note from this logger. 

+

17# * A logger for "annotated_logger.logging_config.logger", which logs all messages as 

+

18# json. This logger does not propagate so that the "annotated_logger.logging_config" 

+

19# logger doesn't also log these messages. This logger is used by a non annotated 

+

20# method, but defines a filter that is annotated with the base annotations defined 

+

21# in `AnnotatedLogger(...`. This is an example of how to add annotations to external 

+

22# logs such as django. Note, the annotations this logger receives are based on the 

+

23# annotations passed in to the `AnnotatedLogger` invocation with the config, 

+

24# The second invocation for "weird" has different annotations. You should be able 

+

25# to have multiple of these with different annotations by invoking `AnnotatedLogger` 

+

26# multiple times and including `disable_existing_loggers` in the later configs. 

+

27# You can also provide custom annotations here if you wish to override the 

+

28# annotations from the annotated logger. 

+

29# 

+

30# Note: When creating multiple loggers, especially when doing so in different 

+

31# files/configs keep in mind that names should be unique or they will override 

+

32# eachother leaving you with a very confusing mess. 

+

33# If you want to see how to more easily merge settings into the default logging 

+

34# dict this package uses see the `actions.py` example. 

+

35LOGGING = { 

+

36 "version": 1, 

+

37 "disable_existing_loggers": False, 

+

38 "filters": { 

+

39 "logging_config.logger_filter": { 

+

40 "annotated_filter": True, 

+

41 # You can override the annotations that would be provided like so 

+

42 # But, if you want to do that you are likely better off 

+

43 # using a filter not associated with an AnnotatedLogger 

+

44 # like the `logging_config.logger_filter_parens` below 

+

45 "annotations": {"config_based_filter": True}, 

+

46 }, 

+

47 "logging_config.logger_filter_parens": { 

+

48 "()": AnnotatedFilter, 

+

49 "annotations": {"decorated": False, "class_based_filter": True}, 

+

50 "class_annotations": {}, 

+

51 "plugins": [ 

+

52 BasePlugin(), 

+

53 RuntimeAnnotationsPlugin({"custom_runtime": lambda _record: True}), 

+

54 ], 

+

55 }, 

+

56 }, 

+

57 "handlers": { 

+

58 "logging_config.annotated_handler": { 

+

59 "class": "logging.StreamHandler", 

+

60 "formatter": "logging_config.annotated_formatter", 

+

61 }, 

+

62 "logging_config.logger_handler": { 

+

63 "class": "logging.StreamHandler", 

+

64 # Note that this handler is specifically attached to 

+

65 # `annotated_logger.logging_config.logger` which we intend to use only as a 

+

66 # logger not generated by a decorator. If you add filters to a handler that 

+

67 # will be invoked when logging from a logger generated by the decorator 

+

68 # AKA, logging via the `annotated_logger` parameter, these filters will 

+

69 # be applied *after* the filters that are dynamically generated/updated 

+

70 # and so can override any annotations that share a name. 

+

71 # In this case, if a function annotated `custom_runtime` or one of the 

+

72 # other fields set in the filters above, that value would be overridden 

+

73 # by the value in the filter set in the config. 

+

74 "filters": [ 

+

75 "logging_config.logger_filter", 

+

76 "logging_config.logger_filter_parens", 

+

77 ], 

+

78 "formatter": "logging_config.annotated_formatter", 

+

79 }, 

+

80 "logging_config.long_handler": { 

+

81 "class": "logging.StreamHandler", 

+

82 "formatter": "logging_config.long_formatter", 

+

83 }, 

+

84 "logging_config.error_handler": { 

+

85 "class": "logging.StreamHandler", 

+

86 "level": "ERROR", 

+

87 "formatter": "logging_config.error_formatter", 

+

88 }, 

+

89 "logging_config.weird_handler": { 

+

90 "class": "logging.StreamHandler", 

+

91 "formatter": "logging_config.weird_formatter", 

+

92 }, 

+

93 }, 

+

94 "formatters": { 

+

95 "logging_config.annotated_formatter": { 

+

96 "class": "pythonjsonlogger.json.JsonFormatter", 

+

97 # Note that this format string uses `time` which is set by the renamer 

+

98 # plugin. It also has `lvl` which is there strictly to test our fallback 

+

99 # to using `levelno` in the mocks to determine level. 

+

100 "format": "{time} {lvl} {name} {runtime} {message}", 

+

101 "style": "{", 

+

102 }, 

+

103 "logging_config.error_formatter": { 

+

104 "format": "{level} {message}", 

+

105 "style": "{", 

+

106 }, 

+

107 "logging_config.long_formatter": { 

+

108 "format": "{lvl} Long message, may be split {message}", 

+

109 # 3.12 added support for defaults in dict configs 

+

110 # With that we can add the format and defaults below 

+

111 # for a more realistic example. Not all of the messages 

+

112 # in the method we set to use this are long enough to be split, 

+

113 # so, some of them don't have the message_part(s) fields. 

+

114 # "format": "{level} {message_part}/{message_parts} {message}", # noqa: ERA001 E501 

+

115 # "defaults": {"message_part": 1, "message_parts": 1}, # noqa: ERA001 

+

116 "style": "{", 

+

117 }, 

+

118 "logging_config.weird_formatter": { 

+

119 "class": "pythonjsonlogger.json.JsonFormatter", 

+

120 "format": "{time} {lvl} {name} {message}", 

+

121 "style": "{", 

+

122 }, 

+

123 }, 

+

124 "loggers": { 

+

125 "annotated_logger.logging_config": { 

+

126 "level": "DEBUG", 

+

127 "handlers": [ 

+

128 "logging_config.annotated_handler", 

+

129 "logging_config.error_handler", 

+

130 ], 

+

131 "propagate": True, 

+

132 }, 

+

133 "annotated_logger.logging_config_weird": { 

+

134 "level": "INFO", 

+

135 "handlers": ["logging_config.weird_handler"], 

+

136 "propagate": True, 

+

137 }, 

+

138 "annotated_logger.logging_config.long": { 

+

139 "level": "INFO", 

+

140 "handlers": ["logging_config.long_handler"], 

+

141 "propagate": True, 

+

142 }, 

+

143 "annotated_logger.logging_config.logger": { 

+

144 "handlers": ["logging_config.logger_handler"], 

+

145 "propagate": False, 

+

146 }, 

+

147 }, 

+

148} 

+

149 

+

150 

+

151def runtime(_record: logging.LogRecord) -> str: 

+

152 """Return the string every time.""" 

+

153 return "this function is called every time" 

+

154 

+

155 

+

156annotated_logger = AnnotatedLogger( 

+

157 annotations={"hostname": "my-host"}, 

+

158 # This is deprecated, use the RuntimeAnnotationsPlugin instead. 

+

159 # This param is kept for backwards compatibility and creates a 

+

160 # RuntimeAnnotationsPlugin instead. 

+

161 # This is left as an example and to provide test coverage. 

+

162 plugins=[ 

+

163 RenamerPlugin(time="created", lvl="levelname"), 

+

164 RuntimeAnnotationsPlugin({"runtime": runtime}), 

+

165 ], 

+

166 log_level=logging.DEBUG, 

+

167 max_length=200, 

+

168 name="annotated_logger.logging_config", 

+

169 config=LOGGING, 

+

170) 

+

171annotate_logs = annotated_logger.annotate_logs 

+

172 

+

173weird_annotated_logger = AnnotatedLogger( 

+

174 annotations={"weird": True}, 

+

175 plugins=[RenamerPlugin(time="created", lvl="levelname")], 

+

176 log_level=logging.INFO, 

+

177 name="annotated_logger.logging_config_weird", 

+

178 config=False, 

+

179) 

+

180weird_annotate_logs = weird_annotated_logger.annotate_logs 

+

181 

+

182logger = logging.getLogger("annotated_logger.logging_config.logger") 

+

183logger.setLevel("DEBUG") 

+

184 

+

185 

+

186def make_some_logs() -> None: 

+

187 """Log messages using a native logging logger.""" 

+

188 logger.debug("this is debug") 

+

189 logger.info("this is info") 

+

190 logger.warning("this is warning") 

+

191 logger.error("this is error") 

+

192 

+

193 

+

194@annotate_logs(_typing_requested=True, _typing_self=False) 

+

195def make_some_annotated_logs(annotated_logger: AnnotatedAdapter) -> None: 

+

196 """Log messages using the provided annotated_logger.""" 

+

197 annotated_logger.debug("this is debug") 

+

198 annotated_logger.info("this is info") 

+

199 annotated_logger.warning("this is warning") 

+

200 annotated_logger.error("this is error") 

+

201 

+

202 

+

203@weird_annotate_logs( 

+

204 _typing_requested=True, 

+

205 _typing_self=False, 

+

206) 

+

207def make_some_weird_logs(annotated_logger: AnnotatedAdapter) -> None: 

+

208 """Log messages using the provided annotated_logger.""" 

+

209 annotated_logger.debug("this is debug") 

+

210 annotated_logger.info("this is info") 

+

211 annotated_logger.warning("this is warning") 

+

212 annotated_logger.error("this is error") 

+

213 

+

214 

+

215@annotate_logs( 

+

216 _typing_requested=True, 

+

217 _typing_self=False, 

+

218 logger_name="annotated_logger.logging_config.long", 

+

219 success_info=False, 

+

220) 

+

221def log_really_long_message(annotated_logger: AnnotatedAdapter) -> None: 

+

222 """Log a message that is so long it will get split.""" 

+

223 message = "1" * 200 + "2" * 200 + "3333" 

+

224 annotated_logger.info(message) 

+

225 annotated_logger.info("4" * 200) 

+

226 annotated_logger.info("5" * 201) 

+

227 annotated_logger.info("6" * 199) 

+
+ + + diff --git a/htmlcov/z_beb44c9891d1179a___init___py.html b/htmlcov/z_beb44c9891d1179a___init___py.html new file mode 100644 index 0000000..4a46dde --- /dev/null +++ b/htmlcov/z_beb44c9891d1179a___init___py.html @@ -0,0 +1,1063 @@ + + + + + Coverage for annotated_logger/__init__.py: 100% + + + + + +
+
+

+ Coverage for annotated_logger/__init__.py: + 100% +

+ +

+ 314 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2 

+

3import contextlib 

+

4import inspect 

+

5import logging 

+

6import logging.config 

+

7import time 

+

8import uuid 

+

9from collections.abc import Iterator 

+

10from copy import copy, deepcopy 

+

11from typing import ( 

+

12 TYPE_CHECKING, 

+

13 Any, 

+

14 Callable, 

+

15 Concatenate, 

+

16 Literal, 

+

17 ParamSpec, 

+

18 Protocol, 

+

19 TypeVar, 

+

20 cast, 

+

21 overload, 

+

22) 

+

23 

+

24from makefun import wraps 

+

25 

+

26from annotated_logger.filter import AnnotatedFilter 

+

27from annotated_logger.plugins import BasePlugin 

+

28 

+

29if TYPE_CHECKING: # pragma: no cover 

+

30 from collections.abc import MutableMapping 

+

31 

+

32# Use 0.0.0.dev1 and so on when working in a PR 

+

33# Each push attempts to upload to testpypi, but it only works with a unique version 

+

34# https://test.pypi.org/project/annotated-logger/ 

+

35# The dev versions in testpypi can then be pulled in to whatever project needed 

+

36# the new feature. 

+

37VERSION = "1.2.3" # pragma: no mutate 

+

38 

+

39T = TypeVar("T") 

+

40P = ParamSpec("P") 

+

41P2 = ParamSpec("P2") 

+

42P3 = ParamSpec("P3") 

+

43R = TypeVar("R") 

+

44S = TypeVar("S") 

+

45S2 = TypeVar("S2") 

+

46C_co = TypeVar("C_co", covariant=True) 

+

47 

+

48 

+

49class AnnotatedClass(Protocol[C_co]): 

+

50 """Protocol for typing classes that we annotate and add the logger to.""" 

+

51 

+

52 annotated_logger: AnnotatedAdapter 

+

53 

+

54 

+

55PreCall = Callable[Concatenate[S, "AnnotatedAdapter", P], None] | None 

+

56PostCall = Callable[Concatenate[S, "AnnotatedAdapter", P], None] | None 

+

57SelfLoggerAndParams = Callable[Concatenate[S, "AnnotatedAdapter", P], R] 

+

58LoggerAndParams = Callable[Concatenate["AnnotatedAdapter", P], R] 

+

59SelfAndParams = Callable[Concatenate[S, P], R] 

+

60ParamsOnly = Callable[P, R] 

+

61SelfAndLogger = Callable[[S, "AnnotatedAdapter"], R] 

+

62LoggerOnly = Callable[["AnnotatedAdapter"], R] 

+

63SelfOnly = Callable[[S], R] 

+

64Empty = Callable[[], R] 

+

65 

+

66NoInjectionSelf = Callable[[SelfAndParams[S, P, R]], SelfAndParams[S, P, R]] 

+

67NoInjectionBare = Callable[[ParamsOnly[P, R]], ParamsOnly[P, R]] 

+

68InjectionSelf = Callable[[SelfLoggerAndParams[S, P, R]], SelfAndParams[S, P, R]] 

+

69InjectionSelfProvide = Callable[ 

+

70 [SelfLoggerAndParams[S, P, R]], SelfLoggerAndParams[S, P, R] 

+

71] 

+

72InjectionBare = Callable[[LoggerAndParams[P, R]], ParamsOnly[P, R]] 

+

73InjectionBareProvide = Callable[[LoggerAndParams[P, R]], LoggerAndParams[P, R]] 

+

74 

+

75Function = ( 

+

76 SelfLoggerAndParams[S, P, R] 

+

77 | SelfAndParams[S, P, R] 

+

78 | SelfAndLogger[S, R] 

+

79 | SelfOnly[S, R] 

+

80 | LoggerAndParams[P, R] 

+

81 | ParamsOnly[P, R] 

+

82 | LoggerOnly[R] 

+

83 | Empty[R] 

+

84) 

+

85Decorator = ( 

+

86 NoInjectionSelf[S, P, R] 

+

87 | InjectionSelf[S, P, R] 

+

88 | InjectionSelfProvide[S, P, R] 

+

89 | NoInjectionBare[P, R] 

+

90 | InjectionBare[P, R] 

+

91 | InjectionBareProvide[P, R] 

+

92) 

+

93Annotations = dict[str, Any] 

+

94 

+

95 

+

96DEFAULT_LOGGING_CONFIG = { 

+

97 "version": 1, 

+

98 "disable_existing_loggers": False, # pragma: no mutate 

+

99 "filters": { 

+

100 "annotated_filter": { 

+

101 "annotated_filter": True, # pragma: no mutate 

+

102 } 

+

103 }, 

+

104 "handlers": { 

+

105 "annotated_handler": { 

+

106 "class": "logging.StreamHandler", 

+

107 "formatter": "annotated_formatter", 

+

108 }, 

+

109 }, 

+

110 "formatters": { 

+

111 "annotated_formatter": { 

+

112 "class": "pythonjsonlogger.json.JsonFormatter", # pragma: no mutate 

+

113 "format": "{created} {levelname} {name} {message}", # pragma: no mutate 

+

114 "style": "{", 

+

115 }, 

+

116 }, 

+

117 "loggers": { 

+

118 "annotated_logger": { 

+

119 "level": "DEBUG", 

+

120 "handlers": ["annotated_handler"], 

+

121 "propagate": False, # pragma: no mutate 

+

122 }, 

+

123 }, 

+

124} 

+

125 

+

126 

+

127class AnnotatedIterator(Iterator[T]): 

+

128 """Iterator that logs as it iterates.""" 

+

129 

+

130 def __init__( 

+

131 self, 

+

132 logger: AnnotatedAdapter, 

+

133 name: str, 

+

134 wrapped: Iterator[T], 

+

135 *, 

+

136 value: bool, 

+

137 level: str, 

+

138 ) -> None: 

+

139 """Store the wrapped iterator, the logger and note if we log the value.""" 

+

140 self.wrapped = wrapped 

+

141 self.logger = logger 

+

142 self.extras: dict[str, T | str] = {"iterator": name} 

+

143 self.value = value 

+

144 log_methods = { 

+

145 "debug": self.logger.debug, 

+

146 "info": self.logger.info, 

+

147 "warning": self.logger.warning, 

+

148 "error": self.logger.error, 

+

149 "exception": self.logger.exception, 

+

150 } 

+

151 self.log_method = log_methods[level] 

+

152 

+

153 def __iter__(self) -> AnnotatedIterator[T]: 

+

154 """Log the start of the iteration.""" 

+

155 self.log_method("Starting iteration", extra=self.extras) 

+

156 return self 

+

157 

+

158 def __next__(self) -> T: 

+

159 """Log that we are at the next iteration.""" 

+

160 try: 

+

161 value = next(self.wrapped) 

+

162 if self.value: 

+

163 self.extras["value"] = value 

+

164 except StopIteration: 

+

165 self.log_method("Execution complete", extra=self.extras) 

+

166 raise 

+

167 

+

168 self.log_method("next", extra=self.extras) 

+

169 return value 

+

170 

+

171 

+

172class AnnotatedAdapter(logging.LoggerAdapter): # pyright: ignore[reportMissingTypeArgument] 

+

173 """Adapter that provides extra methods.""" 

+

174 

+

175 def __init__( 

+

176 self, 

+

177 logger: logging.Logger, 

+

178 annotated_filter: AnnotatedFilter, 

+

179 max_length: int | None = None, 

+

180 ) -> None: 

+

181 """Adapter that acts like a LogRecord, but allows for annotations.""" 

+

182 self.filter = annotated_filter 

+

183 self.logger = logger 

+

184 self.logger.addFilter(annotated_filter) 

+

185 self.max_length = max_length 

+

186 

+

187 # We don't need to send in contextual information here 

+

188 # as we do it in the filter for runtime stuff 

+

189 super().__init__(logger) 

+

190 

+

191 def iterator( 

+

192 self, 

+

193 name: str, 

+

194 wrapped: Iterator[T], 

+

195 *, 

+

196 value: bool = True, 

+

197 level: str = "info", 

+

198 ) -> AnnotatedIterator[T]: 

+

199 """Return an iterator that logs as it iterates.""" 

+

200 return AnnotatedIterator(self, name, wrapped, value=value, level=level) 

+

201 

+

202 def process( 

+

203 self, msg: str, kwargs: MutableMapping[str, Any] 

+

204 ) -> tuple[str, MutableMapping[str, Any]]: 

+

205 """Override default LoggerAdapter process behavior. 

+

206 

+

207 By default a LoggerAdapter replaces the extras passed in a logger call with 

+

208 the ones given at it's initialization. That's not the behavior we want. 

+

209 So, we just return the kwargs as provided instead. 

+

210 

+

211 3.13 adds a `merge_extra` argument which should make this method unneeded. 

+

212 But, it doesn't make sense to force everyone to use python 3.13. 

+

213 """ 

+

214 return msg, kwargs 

+

215 

+

216 def log( 

+

217 self, 

+

218 level: int, 

+

219 msg: object, 

+

220 *args: object, 

+

221 **kwargs: object, 

+

222 ) -> None: 

+

223 """Override log method to allow for message splitting.""" 

+

224 if not self.max_length or not isinstance(msg, str): 

+

225 return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType] 

+

226 

+

227 msg_len = len(msg) # pyright: ignore[reportArgumentType] 

+

228 if msg_len <= self.max_length: 

+

229 return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType] 

+

230 

+

231 msg_chunks = [] 

+

232 while len(msg) > self.max_length: # pyright: ignore[reportArgumentType] # pragma: no mutate 

+

233 msg_chunks.append(msg[: self.max_length]) # pyright: ignore[reportArgumentType] 

+

234 msg = msg[self.max_length :] # pyright: ignore[reportArgumentType] 

+

235 kwargs["extra"] = {"message_parts": len(msg_chunks) + 1, "split": True} 

+

236 kwargs["extra"]["split_complete"] = False 

+

237 for i, part in enumerate(msg_chunks): 

+

238 kwargs["extra"]["message_part"] = i + 1 

+

239 super().log( 

+

240 level, 

+

241 part, 

+

242 *args, 

+

243 **kwargs, # pyright: ignore[reportArgumentType] 

+

244 ) 

+

245 kwargs["extra"]["message_part"] = len(msg_chunks) + 1 

+

246 kwargs["extra"]["split_complete"] = True 

+

247 return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType] 

+

248 

+

249 def annotate(self, *, persist: bool = False, **kwargs: Any) -> None: 

+

250 """Add an annotation to the filter.""" 

+

251 if persist: 

+

252 self.filter.class_annotations.update(kwargs) 

+

253 else: 

+

254 self.filter.annotations.update(kwargs) 

+

255 

+

256 

+

257class AnnotatedLogger: 

+

258 """Class that contains settings and the decorator method. 

+

259 

+

260 Args: 

+

261 ---- 

+

262 annotations: Dictionary of annotations to be added to every log message 

+

263 plugins: list of plugins to use 

+

264 

+

265 Methods: 

+

266 ------- 

+

267 annotate_logs: Decorator that will insert the `annotated_logger` argument if 

+

268 asked for in the method signature or let a provided AnnotatedAdapter to be 

+

269 passed. Creates a new AnnotatedAdapter instance for each invocation of a 

+

270 annotated function to isolate any annotations that are set during execution. 

+

271 

+

272 """ 

+

273 

+

274 def __init__( # noqa: PLR0913 

+

275 self, 

+

276 annotations: dict[str, Any] | None = None, 

+

277 plugins: list[BasePlugin] | None = None, 

+

278 max_length: int | None = None, 

+

279 log_level: int = logging.INFO, 

+

280 name: str = "annotated_logger", 

+

281 config: dict[str, Any] | Literal[False] | None = None, 

+

282 ) -> None: 

+

283 """Store the settings. 

+

284 

+

285 Args: 

+

286 ---- 

+

287 annotations: Dictionary of static annotations - default None 

+

288 plugins: List of plugins to be applied - default [BasePlugin] 

+

289 is created and used - default None 

+

290 max_length: Integer, maximum length of a message before it's broken into 

+

291 multiple message and log calls. - default None 

+

292 log_level: Integer, log level set for the shared root logger of the package. 

+

293 - default logging.INFO (20) 

+

294 name: Name of the shared root logger of the package. If more than one 

+

295 `AnnotatedLogger` object is created in a project this should be set, 

+

296 otherwise settings like level will be overwritten by the second to execute 

+

297 - default 'annotated_logger' 

+

298 config: Optional - logging config dictionary to be passed to 

+

299 logging.config.dictConfig or False. If false dictConfig will not be called. 

+

300 If not passed the DEFAULT_LOGGING_CONFIG will be used. A special 

+

301 `annotated_filter` keyword is looked for, if present it will be 

+

302 replaced with a `()` filter config to generate a filter for this 

+

303 instance of `AnnotatedLogger`. 

+

304 

+

305 """ 

+

306 if plugins is None: 

+

307 plugins = [] 

+

308 

+

309 self.log_level = log_level 

+

310 self.logger_root_name = name 

+

311 self.logger_base = logging.getLogger(self.logger_root_name) 

+

312 self.logger_base.setLevel(self.log_level) 

+

313 self.annotations = annotations or {} 

+

314 self.plugins = [BasePlugin()] 

+

315 self.plugins.extend(plugins) 

+

316 

+

317 if config is None: 

+

318 config = deepcopy(DEFAULT_LOGGING_CONFIG) 

+

319 if config: 

+

320 for config_filter in config["filters"].values(): 

+

321 if config_filter.get("annotated_filter"): 

+

322 del config_filter["annotated_filter"] 

+

323 config_filter["()"] = self.generate_filter 

+

324 

+

325 # If we pass in config=False we don't want to configure. 

+

326 # This is typically because we have another AnnotatedLogger 

+

327 # object which did run the config and the dict config had config 

+

328 # for both. 

+

329 if config: 

+

330 logging.config.dictConfig(config) 

+

331 

+

332 self.max_length = max_length 

+

333 

+

334 def _generate_logger( 

+

335 self, 

+

336 function: Function[S, P, R] | None = None, 

+

337 cls: type | None = None, 

+

338 logger_base_name: str | None = None, 

+

339 ) -> AnnotatedAdapter: 

+

340 """Generate a unique adapter with a unique logger object. 

+

341 

+

342 This is required because the AnnotatedAdapter adds a filter to the logger. 

+

343 The filter stores the annotations inside it, so they will mix if a new filter 

+

344 and logger are not created each time. 

+

345 """ 

+

346 root_name = logger_base_name or self.logger_root_name 

+

347 logger = logging.getLogger( 

+

348 f"{root_name}.{uuid.uuid4()}" # pragma: no mutate 

+

349 ) 

+

350 

+

351 annotated_filter = self.generate_filter(function=function, cls=cls) 

+

352 

+

353 return AnnotatedAdapter(logger, annotated_filter, self.max_length) 

+

354 

+

355 def _action_annotation( 

+

356 self, function: Function[S, P, R], key: str = "action" 

+

357 ) -> dict[str, str]: 

+

358 return {key: f"{function.__module__}:{function.__qualname__}"} 

+

359 

+

360 def generate_filter( 

+

361 self, 

+

362 function: Function[S, P, R] | None = None, 

+

363 cls: type[C_co] | None = None, 

+

364 annotations: dict[str, Any] | None = None, 

+

365 ) -> AnnotatedFilter: 

+

366 """Create a AnnotatedFilter with the correct annotations and plugins.""" 

+

367 annotations_passed = annotations 

+

368 annotations = annotations or {} 

+

369 if function: 

+

370 annotations.update(self._action_annotation(function)) 

+

371 class_annotations = {} 

+

372 elif cls: 

+

373 class_annotations = {"class": f"{cls.__module__}:{cls.__qualname__}"} 

+

374 else: 

+

375 class_annotations = {} 

+

376 if not annotations_passed: 

+

377 annotations.update(self.annotations) 

+

378 

+

379 return AnnotatedFilter( 

+

380 annotations=annotations, 

+

381 class_annotations=class_annotations, 

+

382 plugins=self.plugins, 

+

383 ) 

+

384 

+

385 #### Defaults 

+

386 @overload 

+

387 def annotate_logs( 

+

388 self, 

+

389 logger_name: str | None = None, 

+

390 *, 

+

391 success_info: bool = True, # pragma: no mutate 

+

392 pre_call: PreCall[S2, P2] = None, 

+

393 post_call: PostCall[S2, P3] = None, 

+

394 ) -> NoInjectionSelf[S, P, R]: ... 

+

395 

+

396 @overload 

+

397 def annotate_logs( 

+

398 self, 

+

399 logger_name: str | None = None, 

+

400 *, 

+

401 success_info: bool = True, # pragma: no mutate 

+

402 pre_call: PreCall[S2, P2] = None, 

+

403 post_call: PostCall[S2, P3] = None, 

+

404 _typing_requested: Literal[False], 

+

405 ) -> NoInjectionSelf[S, P, R]: ... 

+

406 

+

407 @overload 

+

408 def annotate_logs( 

+

409 self, 

+

410 logger_name: str | None = None, 

+

411 *, 

+

412 success_info: bool = True, # pragma: no mutate 

+

413 pre_call: PreCall[S2, P2] = None, 

+

414 post_call: PostCall[S2, P3] = None, 

+

415 provided: Literal[False], 

+

416 ) -> NoInjectionSelf[S, P, R]: ... 

+

417 

+

418 @overload 

+

419 def annotate_logs( 

+

420 self, 

+

421 logger_name: str | None = None, 

+

422 *, 

+

423 success_info: bool = True, # pragma: no mutate 

+

424 pre_call: PreCall[S2, P2] = None, 

+

425 post_call: PostCall[S2, P3] = None, 

+

426 _typing_self: Literal[True], 

+

427 ) -> NoInjectionSelf[S, P, R]: ... 

+

428 

+

429 @overload 

+

430 def annotate_logs( 

+

431 self, 

+

432 logger_name: str | None = None, 

+

433 *, 

+

434 success_info: bool = True, # pragma: no mutate 

+

435 pre_call: PreCall[S2, P2] = None, 

+

436 post_call: PostCall[S2, P3] = None, 

+

437 ) -> NoInjectionSelf[S, P, R]: ... 

+

438 

+

439 @overload 

+

440 def annotate_logs( 

+

441 self, 

+

442 logger_name: str | None = None, 

+

443 *, 

+

444 success_info: bool = True, # pragma: no mutate 

+

445 pre_call: PreCall[S2, P2] = None, 

+

446 post_call: PostCall[S2, P3] = None, 

+

447 _typing_self: Literal[True], 

+

448 _typing_requested: Literal[False], 

+

449 ) -> NoInjectionSelf[S, P, R]: ... 

+

450 

+

451 @overload 

+

452 def annotate_logs( 

+

453 self, 

+

454 logger_name: str | None = None, 

+

455 *, 

+

456 success_info: bool = True, # pragma: no mutate 

+

457 pre_call: PreCall[S2, P2] = None, 

+

458 post_call: PostCall[S2, P3] = None, 

+

459 provided: Literal[False], 

+

460 _typing_requested: Literal[False], 

+

461 ) -> NoInjectionSelf[S, P, R]: ... 

+

462 

+

463 @overload 

+

464 def annotate_logs( 

+

465 self, 

+

466 logger_name: str | None = None, 

+

467 *, 

+

468 success_info: bool = True, # pragma: no mutate 

+

469 pre_call: PreCall[S2, P2] = None, 

+

470 post_call: PostCall[S2, P3] = None, 

+

471 _typing_self: Literal[True], 

+

472 provided: Literal[False], 

+

473 ) -> NoInjectionSelf[S, P, R]: ... 

+

474 

+

475 @overload 

+

476 def annotate_logs( 

+

477 self, 

+

478 logger_name: str | None = None, 

+

479 *, 

+

480 success_info: bool = True, # pragma: no mutate 

+

481 pre_call: PreCall[S2, P2] = None, 

+

482 post_call: PostCall[S2, P3] = None, 

+

483 _typing_self: Literal[True], 

+

484 _typing_requested: Literal[False], 

+

485 provided: Literal[False], 

+

486 ) -> NoInjectionSelf[S, P, R]: ... 

+

487 

+

488 #### Class True 

+

489 @overload 

+

490 def annotate_logs( 

+

491 self, 

+

492 logger_name: str | None = None, 

+

493 *, 

+

494 _typing_class: Literal[True], 

+

495 success_info: bool = True, # pragma: no mutate 

+

496 pre_call: PreCall[S, P2] = None, 

+

497 post_call: PostCall[S, P3] = None, 

+

498 ) -> Callable[[type[C_co]], type[C_co]]: ... 

+

499 

+

500 ### Instance False 

+

501 @overload 

+

502 def annotate_logs( 

+

503 self, 

+

504 logger_name: str | None = None, 

+

505 *, 

+

506 success_info: bool = True, # pragma: no mutate 

+

507 pre_call: PreCall[S2, P2] = None, 

+

508 post_call: PostCall[S2, P3] = None, 

+

509 _typing_self: Literal[False], 

+

510 ) -> NoInjectionBare[P, R]: ... 

+

511 

+

512 @overload 

+

513 def annotate_logs( 

+

514 self, 

+

515 logger_name: str | None = None, 

+

516 *, 

+

517 success_info: bool = True, # pragma: no mutate 

+

518 pre_call: PreCall[S2, P2] = None, 

+

519 post_call: PostCall[S2, P3] = None, 

+

520 _typing_self: Literal[False], 

+

521 _typing_requested: Literal[False], 

+

522 ) -> NoInjectionBare[P, R]: ... 

+

523 

+

524 @overload 

+

525 def annotate_logs( 

+

526 self, 

+

527 logger_name: str | None = None, 

+

528 *, 

+

529 success_info: bool = True, # pragma: no mutate 

+

530 pre_call: PreCall[S2, P2] = None, 

+

531 post_call: PostCall[S2, P3] = None, 

+

532 _typing_self: Literal[False], 

+

533 provided: Literal[False], 

+

534 ) -> NoInjectionBare[P, R]: ... 

+

535 

+

536 @overload 

+

537 def annotate_logs( 

+

538 self, 

+

539 logger_name: str | None = None, 

+

540 *, 

+

541 success_info: bool = True, # pragma: no mutate 

+

542 pre_call: PreCall[S2, P2] = None, 

+

543 post_call: PostCall[S2, P3] = None, 

+

544 _typing_self: Literal[False], 

+

545 _typing_requested: Literal[False], 

+

546 provided: Literal[False], 

+

547 ) -> NoInjectionBare[P, R]: ... 

+

548 

+

549 ### Requested True 

+

550 @overload 

+

551 def annotate_logs( 

+

552 self, 

+

553 logger_name: str | None = None, 

+

554 *, 

+

555 success_info: bool = True, # pragma: no mutate 

+

556 pre_call: PreCall[S2, P2] = None, 

+

557 post_call: PostCall[S2, P3] = None, 

+

558 _typing_requested: Literal[True], 

+

559 ) -> InjectionSelf[S, P, R]: ... 

+

560 

+

561 @overload 

+

562 def annotate_logs( 

+

563 self, 

+

564 logger_name: str | None = None, 

+

565 *, 

+

566 success_info: bool = True, # pragma: no mutate 

+

567 pre_call: PreCall[S2, P2] = None, 

+

568 post_call: PostCall[S2, P3] = None, 

+

569 _typing_self: Literal[True], 

+

570 _typing_requested: Literal[True], 

+

571 ) -> InjectionSelf[S, P, R]: ... 

+

572 

+

573 @overload 

+

574 def annotate_logs( 

+

575 self, 

+

576 logger_name: str | None = None, 

+

577 *, 

+

578 success_info: bool = True, # pragma: no mutate 

+

579 pre_call: PreCall[S2, P2] = None, 

+

580 post_call: PostCall[S2, P3] = None, 

+

581 provided: Literal[False], 

+

582 _typing_requested: Literal[True], 

+

583 ) -> InjectionSelf[S, P, R]: ... 

+

584 

+

585 @overload 

+

586 def annotate_logs( 

+

587 self, 

+

588 logger_name: str | None = None, 

+

589 *, 

+

590 success_info: bool = True, # pragma: no mutate 

+

591 pre_call: PreCall[S2, P2] = None, 

+

592 post_call: PostCall[S2, P3] = None, 

+

593 _typing_self: Literal[True], 

+

594 _typing_requested: Literal[True], 

+

595 provided: Literal[False], 

+

596 ) -> InjectionSelf[S, P, R]: ... 

+

597 

+

598 ### Provided True, Requested True 

+

599 # Can't provide it if it was not requested, 

+

600 # so no overloads for not requested, but provided 

+

601 @overload 

+

602 def annotate_logs( 

+

603 self, 

+

604 logger_name: str | None = None, 

+

605 *, 

+

606 success_info: bool = True, # pragma: no mutate 

+

607 pre_call: PreCall[S2, P2] = None, 

+

608 post_call: PostCall[S2, P3] = None, 

+

609 _typing_requested: Literal[True], 

+

610 provided: Literal[True], 

+

611 ) -> InjectionSelfProvide[S, P, R]: ... 

+

612 

+

613 @overload 

+

614 def annotate_logs( 

+

615 self, 

+

616 logger_name: str | None = None, 

+

617 *, 

+

618 success_info: bool = True, # pragma: no mutate 

+

619 pre_call: PreCall[S2, P2] = None, 

+

620 post_call: PostCall[S2, P3] = None, 

+

621 _typing_self: Literal[True], 

+

622 _typing_requested: Literal[True], 

+

623 provided: Literal[True], 

+

624 ) -> InjectionSelfProvide[S, P, R]: ... 

+

625 

+

626 ### Instance False, Requested True 

+

627 @overload 

+

628 def annotate_logs( 

+

629 self, 

+

630 logger_name: str | None = None, 

+

631 *, 

+

632 success_info: bool = True, # pragma: no mutate 

+

633 pre_call: PreCall[S2, P2] = None, 

+

634 post_call: PostCall[S2, P3] = None, 

+

635 _typing_self: Literal[False], 

+

636 _typing_requested: Literal[True], 

+

637 ) -> InjectionBare[P, R]: ... 

+

638 

+

639 ### Instance False, Requested True, Provided True 

+

640 # Same not as above that you can't provide if not requested 

+

641 @overload 

+

642 def annotate_logs( 

+

643 self, 

+

644 logger_name: str | None = None, 

+

645 *, 

+

646 success_info: bool = True, # pragma: no mutate 

+

647 pre_call: PreCall[S2, P2] = None, 

+

648 post_call: PostCall[S2, P2] = None, 

+

649 _typing_self: Literal[False], 

+

650 _typing_requested: Literal[True], 

+

651 provided: Literal[True], 

+

652 ) -> InjectionBareProvide[P, R]: ... 

+

653 

+

654 # Between the overloads and the two inner method definitions, 

+

655 # there's not much I can do to reduce the complexity more. 

+

656 # So, ignoring the complexity metric 

+

657 def annotate_logs( # noqa: C901 

+

658 self, 

+

659 logger_name: str | None = None, 

+

660 *, 

+

661 success_info: bool = True, 

+

662 pre_call: PreCall[S2, P2] = None, 

+

663 post_call: PostCall[S2, P3] = None, 

+

664 provided: bool = False, 

+

665 _typing_self: bool = True, # pragma: no mutate 

+

666 _typing_requested: bool = False, # pragma: no mutate 

+

667 _typing_class: bool = False, # pragma: no mutate 

+

668 ) -> Decorator[S, P, R] | Callable[[type[C_co]], type[C_co]]: 

+

669 """Log start and end of function and provide an annotated logger if requested. 

+

670 

+

671 Args: 

+

672 ---- 

+

673 logger_name: Optional - Specify the name of the logger attached to 

+

674 the decorated function. 

+

675 success_info: Log success at an info level, if falsey success will be 

+

676 logged at debug. Default: True 

+

677 provided: Boolean that indicates the caller will be providing it's 

+

678 own annotated_logger. Default: False 

+

679 pre_call: Method that takes the same arguments as the decorated function 

+

680 and does something. Called before the function and the `start` log message. 

+

681 post_call: Method that takes the same arguments as the decorated function 

+

682 and does something. Called after the function and before the `success` 

+

683 log message or in the exception handling. 

+

684 _typing_self: Used only for type hint overloads. Indicates that the 

+

685 decorated method is an instance method and has a self parameter. 

+

686 Default: True 

+

687 _typing_requested: Used only for type hint overloads. Indicates that the 

+

688 decorated method is expecting an annotated_logger to be provided. 

+

689 Default: False 

+

690 

+

691 Notes: 

+

692 ----- 

+

693 In order to fully support type hinting, the annotated_logger argument 

+

694 must be the first argument (after self/cls). Type hinting will only work 

+

695 correctly if the _typing arguments are set correctly, but the code will 

+

696 work fine at runtime without the _typing arguments. 

+

697 

+

698 """ 

+

699 

+

700 @overload 

+

701 def decorator( 

+

702 wrapped: SelfLoggerAndParams[S, P, R], 

+

703 ) -> SelfAndParams[S, P, R] | SelfLoggerAndParams[S, P, R]: ... 

+

704 

+

705 @overload 

+

706 def decorator( 

+

707 wrapped: LoggerAndParams[P, R], 

+

708 ) -> ParamsOnly[P, R] | LoggerAndParams[P, R]: ... 

+

709 

+

710 @overload 

+

711 def decorator( 

+

712 wrapped: SelfAndParams[S, P, R], 

+

713 ) -> SelfAndParams[S, P, R]: ... 

+

714 

+

715 @overload 

+

716 def decorator( 

+

717 wrapped: ParamsOnly[P, R], 

+

718 ) -> ParamsOnly[P, R] | Empty[R]: ... 

+

719 

+

720 @overload 

+

721 def decorator(wrapped: type[C_co]) -> Callable[P, AnnotatedClass[C_co]]: ... 

+

722 

+

723 def decorator( # noqa: C901 

+

724 wrapped: Function[S, P, R] | type[C_co], 

+

725 ) -> Function[S, P, R] | Callable[P, AnnotatedClass[C_co]]: 

+

726 if isinstance(wrapped, type): 

+

727 

+

728 def wrap_class( 

+

729 *args: P.args, **kwargs: P.kwargs 

+

730 ) -> AnnotatedClass[C_co]: 

+

731 logger = self._generate_logger( 

+

732 cls=wrapped, logger_base_name=logger_name 

+

733 ) 

+

734 logger.debug("init") 

+

735 new = cast(AnnotatedClass[C_co], wrapped(*args, **kwargs)) 

+

736 new.annotated_logger = logger 

+

737 return new 

+

738 

+

739 return wrap_class 

+

740 

+

741 (remove_args, inject_logger) = self._determine_signature_adjustments( 

+

742 wrapped, provided=provided 

+

743 ) 

+

744 

+

745 @wraps( 

+

746 wrapped, 

+

747 remove_args=remove_args, 

+

748 ) 

+

749 def wrap_function(*args: P.args, **kwargs: P.kwargs) -> R: 

+

750 __tracebackhide__ = True # pragma: no mutate 

+

751 

+

752 post_call_attempted = False # pragma: no mutate 

+

753 

+

754 new_args, new_kwargs, logger, pre_execution_annotations = inject_logger( 

+

755 list(args), kwargs, logger_base_name=logger_name 

+

756 ) 

+

757 try: 

+

758 start_time = time.perf_counter() 

+

759 if pre_call: 

+

760 pre_call(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue] 

+

761 logger.debug("start") 

+

762 

+

763 result = wrapped(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue] 

+

764 logger.annotate(success=True) 

+

765 if post_call: 

+

766 post_call_attempted = True 

+

767 _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue] 

+

768 end_time = time.perf_counter() 

+

769 logger.annotate(run_time=f"{end_time - start_time :.1f}") 

+

770 with contextlib.suppress(TypeError): 

+

771 logger.annotate(count=len(result)) # pyright: ignore[reportArgumentType] 

+

772 

+

773 if success_info: 

+

774 logger.info("success") 

+

775 else: 

+

776 logger.debug("success") 

+

777 

+

778 # If we were provided with a logger object, set the annotations 

+

779 # back to what they were before the wrapped method was called. 

+

780 if pre_execution_annotations: 

+

781 logger.filter.annotations = pre_execution_annotations 

+

782 except Exception as e: 

+

783 for plugin in logger.filter.plugins: 

+

784 logger = plugin.uncaught_exception(e, logger) 

+

785 logger.exception( 

+

786 "Uncaught Exception in logged function", 

+

787 ) 

+

788 if post_call and not post_call_attempted: 

+

789 _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue] 

+

790 raise 

+

791 return result 

+

792 

+

793 return wrap_function 

+

794 

+

795 return decorator 

+

796 

+

797 def _determine_signature_adjustments( 

+

798 self, 

+

799 function: Function[S, P, R], 

+

800 *, 

+

801 provided: bool, 

+

802 ) -> tuple[ 

+

803 list[str], 

+

804 Callable[ 

+

805 Concatenate[list[Any], dict[str, Any], ...], 

+

806 tuple[list[Any], dict[str, Any], AnnotatedAdapter, Annotations | None], 

+

807 ], 

+

808 ]: 

+

809 written_signature = inspect.signature(function) 

+

810 logger_requested = False # pragma: no mutate 

+

811 remove_args = [] 

+

812 index, instance_method = self._check_parameters_for_self_and_cls( 

+

813 written_signature 

+

814 ) 

+

815 if "annotated_logger" in written_signature.parameters: 

+

816 if list(written_signature.parameters.keys())[index] != "annotated_logger": 

+

817 error_message = "annotated_logger must be the first argument" 

+

818 raise TypeError(error_message) 

+

819 

+

820 logger_requested = True 

+

821 if not provided: 

+

822 remove_args = ["annotated_logger"] 

+

823 

+

824 def inject_logger( 

+

825 args: list[Any], 

+

826 kwargs: dict[str, Any], 

+

827 logger_base_name: str | None = None, 

+

828 ) -> tuple[list[Any], dict[str, Any], AnnotatedAdapter, Annotations | None]: 

+

829 if not logger_requested: 

+

830 logger = self._generate_logger( 

+

831 function, logger_base_name=logger_base_name 

+

832 ) 

+

833 return (args, kwargs, logger, None) 

+

834 

+

835 by_index = False # pragma: no mutate 

+

836 # Check for a var positional or positional only 

+

837 # If present that means we'll have values in args when invoking 

+

838 # but, if not everything will be in kwargs 

+

839 for v in written_signature.parameters.values(): 

+

840 if v.kind == inspect.Parameter.VAR_POSITIONAL: 

+

841 by_index = True 

+

842 

+

843 new_args = copy(args) 

+

844 new_kwargs = copy(kwargs) 

+

845 if by_index: 

+

846 logger, annotations, new_args = self._inject_by_index( 

+

847 provided=provided, 

+

848 instance_method=instance_method, 

+

849 args=new_args, 

+

850 index=index, 

+

851 function=function, 

+

852 logger_base_name=logger_base_name, 

+

853 ) 

+

854 else: 

+

855 logger, annotations, new_kwargs = self._inject_by_kwarg( 

+

856 provided=provided, 

+

857 instance_method=instance_method, 

+

858 kwargs=new_kwargs, 

+

859 function=function, 

+

860 logger_base_name=logger_base_name, 

+

861 ) 

+

862 

+

863 return new_args, new_kwargs, logger, annotations 

+

864 

+

865 return remove_args, inject_logger 

+

866 

+

867 def _inject_by_kwarg( 

+

868 self, 

+

869 *, 

+

870 provided: bool, 

+

871 instance_method: bool, 

+

872 function: Function[S, P, R], 

+

873 kwargs: dict[str, Any], 

+

874 logger_base_name: str | None = None, 

+

875 ) -> tuple[AnnotatedAdapter, Annotations | None, dict[str, Any]]: 

+

876 if provided: 

+

877 instance = kwargs["annotated_logger"] 

+

878 elif instance_method: 

+

879 instance = kwargs["self"] 

+

880 else: 

+

881 instance = False # pragma: no mutate 

+

882 logger, annotations = self._pick_correct_logger( 

+

883 function, instance, logger_base_name=logger_base_name 

+

884 ) 

+

885 if not provided: 

+

886 kwargs["annotated_logger"] = logger 

+

887 

+

888 return logger, annotations, kwargs 

+

889 

+

890 def _inject_by_index( # noqa: PLR0913 

+

891 self, 

+

892 *, 

+

893 provided: bool, 

+

894 instance_method: bool, 

+

895 function: Function[S, P, R], 

+

896 args: list[Any], 

+

897 index: int, 

+

898 logger_base_name: str | None = None, 

+

899 ) -> tuple[AnnotatedAdapter, Annotations | None, list[Any]]: 

+

900 if provided: 

+

901 instance = args[index] 

+

902 elif instance_method: 

+

903 instance = args[0] 

+

904 else: 

+

905 instance = False # pragma: no mutate 

+

906 logger, annotations = self._pick_correct_logger( 

+

907 function, instance, logger_base_name=logger_base_name 

+

908 ) 

+

909 if not provided: 

+

910 args.insert(index, logger) 

+

911 return logger, annotations, args 

+

912 

+

913 def _check_parameters_for_self_and_cls( 

+

914 self, sig: inspect.Signature 

+

915 ) -> tuple[int, bool]: 

+

916 parameters = sig.parameters 

+

917 index = 0 

+

918 instance_method = False 

+

919 if "self" in parameters: 

+

920 index = 1 

+

921 instance_method = True 

+

922 if "cls" in parameters: 

+

923 index = 1 

+

924 

+

925 return index, instance_method 

+

926 

+

927 def _pick_correct_logger( 

+

928 self, 

+

929 function: Function[S, P, R], 

+

930 instance: object | bool, 

+

931 logger_base_name: str | None = None, 

+

932 ) -> tuple[AnnotatedAdapter, Annotations | None]: 

+

933 """Use the instance's logger and annotations if present.""" 

+

934 if instance and hasattr(instance, "annotated_logger"): 

+

935 logger = instance.annotated_logger # pyright: ignore[reportAttributeAccessIssue] 

+

936 annotations = copy(logger.filter.annotations) 

+

937 logger.filter.annotations.update(self._action_annotation(function)) 

+

938 return (logger, annotations) 

+

939 

+

940 if isinstance(instance, AnnotatedAdapter): 

+

941 logger = instance 

+

942 annotations = copy(logger.filter.annotations) 

+

943 logger.filter.annotations.update( 

+

944 self._action_annotation(function, key="subaction") 

+

945 ) 

+

946 return (logger, annotations) 

+

947 

+

948 return ( 

+

949 self._generate_logger(function, logger_base_name=logger_base_name), 

+

950 None, 

+

951 ) 

+

952 

+

953 

+

954def _attempt_post_call( 

+

955 post_call: Callable[P, None], 

+

956 logger: AnnotatedAdapter, 

+

957 *args: P.args, 

+

958 **kwargs: P.kwargs, 

+

959) -> None: 

+

960 try: 

+

961 if post_call: 

+

962 post_call(*args, **kwargs) # pyright: ignore[reportCallIssue] 

+

963 except Exception: 

+

964 logger.annotate(success=False) 

+

965 logger.exception("Post call failed") 

+

966 raise 

+
+ + + diff --git a/htmlcov/z_beb44c9891d1179a_filter_py.html b/htmlcov/z_beb44c9891d1179a_filter_py.html new file mode 100644 index 0000000..45c13a9 --- /dev/null +++ b/htmlcov/z_beb44c9891d1179a_filter_py.html @@ -0,0 +1,155 @@ + + + + + Coverage for annotated_logger/filter.py: 100% + + + + + +
+
+

+ Coverage for annotated_logger/filter.py: + 100% +

+ +

+ 31 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2 

+

3import logging 

+

4from copy import copy 

+

5from typing import Any 

+

6 

+

7import annotated_logger 

+

8 

+

9Annotations = dict[str, Any] 

+

10 

+

11 

+

12class AnnotatedFilter(logging.Filter): 

+

13 """Filter class that stores the annotations and plugins.""" 

+

14 

+

15 def __init__( 

+

16 self, 

+

17 annotations: Annotations | None = None, 

+

18 class_annotations: Annotations | None = None, 

+

19 plugins: list[annotated_logger.BasePlugin] | None = None, 

+

20 ) -> None: 

+

21 """Store the annotations, attributes and plugins.""" 

+

22 self.annotations = annotations or {} 

+

23 self.class_annotations = class_annotations or {} 

+

24 self.plugins = plugins or [annotated_logger.BasePlugin()] 

+

25 

+

26 # This allows plugins to determine what fields were added by the user 

+

27 # vs the ones native to the log record 

+

28 # TODO(crimsonknave): Make a test for this # noqa: TD003, FIX002 

+

29 self.base_attributes = logging.makeLogRecord({}).__dict__ # pragma: no mutate 

+

30 

+

31 def _all_annotations(self) -> Annotations: 

+

32 annotations = {} 

+

33 annotations.update(copy(self.class_annotations)) 

+

34 annotations.update(copy(self.annotations)) 

+

35 annotations["annotated"] = True 

+

36 return annotations 

+

37 

+

38 def filter(self, record: logging.LogRecord) -> bool: 

+

39 """Add the annotations to the record and allow plugins to filter the record. 

+

40 

+

41 The `filter` method is called on each plugin in the order they are listed. 

+

42 The plugin is then able to maniuplate the record object before the next plugin 

+

43 sees it. Returning False from the filter method will stop the evaluation and 

+

44 the log record won't be emitted. 

+

45 """ 

+

46 record.__dict__.update(self._all_annotations()) 

+

47 for plugin in self.plugins: 

+

48 try: 

+

49 result = plugin.filter(record) 

+

50 except Exception: # noqa: BLE001 

+

51 failed_plugins = record.__dict__.get("failed_plugins", []) 

+

52 failed_plugins.append(str(plugin.__class__)) 

+

53 record.__dict__["failed_plugins"] = failed_plugins 

+

54 result = True 

+

55 

+

56 if not result: 

+

57 return False 

+

58 return True 

+
+ + + diff --git a/htmlcov/z_beb44c9891d1179a_mocks_py.html b/htmlcov/z_beb44c9891d1179a_mocks_py.html new file mode 100644 index 0000000..707e75a --- /dev/null +++ b/htmlcov/z_beb44c9891d1179a_mocks_py.html @@ -0,0 +1,350 @@ + + + + + Coverage for annotated_logger/mocks.py: 100% + + + + + +
+
+

+ Coverage for annotated_logger/mocks.py: + 100% +

+ +

+ 124 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2 

+

3import logging 

+

4from typing import Any, Literal 

+

5 

+

6import pychoir 

+

7import pytest 

+

8 

+

9 

+

10class AssertLogged: 

+

11 """Stores the data from a call to `assert_logged` and checks if there is a match.""" 

+

12 

+

13 def __init__( 

+

14 self, 

+

15 level: str | pychoir.core.Matcher, 

+

16 message: str | pychoir.core.Matcher, 

+

17 present: dict[str, str], 

+

18 absent: set[str] | Literal["ALL"], 

+

19 *, 

+

20 count: int | pychoir.core.Matcher, 

+

21 ) -> None: 

+

22 """Store the arguments that were passed to `assert_logged` and set defaults.""" 

+

23 self.level = level 

+

24 self.message = message 

+

25 self.present = present 

+

26 self.absent = absent 

+

27 self.count = count 

+

28 self.found = 0 

+

29 self.failed_matches: dict[str, int] = {} 

+

30 

+

31 def check(self, mock: AnnotatedLogMock) -> None: 

+

32 """Loop through calls in passed mock and check for matches.""" 

+

33 for record in mock.records: 

+

34 differences = self._check_record_matches(record) 

+

35 if len(differences) == 0: 

+

36 self.found = self.found + 1 

+

37 diff_str = str(differences) 

+

38 if diff_str in self.failed_matches: 

+

39 self.failed_matches[diff_str] += 1 

+

40 else: 

+

41 self.failed_matches[diff_str] = 1 

+

42 

+

43 fail_message = self.build_message() 

+

44 if len(fail_message) > 0: 

+

45 pytest.fail("\n".join(fail_message)) 

+

46 

+

47 def _failed_sort_key(self, failed_tuple: tuple[str, int]) -> str: 

+

48 failed, count = failed_tuple 

+

49 message_match = failed.count("Desired message") 

+

50 count_diff = 0 # pragma: no mutate 

+

51 if isinstance(self.count, int): 

+

52 count_diff = abs(count - self.count) 

+

53 number = ( 

+

54 failed.count("Desired") 

+

55 + failed.count("Missing key") 

+

56 + failed.count("Unwanted key") 

+

57 ) 

+

58 length = len(failed) 

+

59 # This will order by if the message matched then how the count differs 

+

60 # then number of incorrect bits and finally the length 

+

61 return f"{message_match}-{count_diff:04d}-{number:04d}-{length:04d}" # pragma: no mutate # noqa: E501 

+

62 

+

63 def build_message(self) -> list[str]: 

+

64 """Create failure message.""" 

+

65 if self.count == 0 and self.found == 0: 

+

66 return [] 

+

67 if self.found == 0: 

+

68 fail_message = [ 

+

69 f"No matching log record found. There were {sum(self.failed_matches.values())} log messages.", # noqa: E501 

+

70 ] 

+

71 

+

72 fail_message.append("Desired:") 

+

73 if isinstance(self.count, int): 

+

74 fail_message.append(f"Count: {self.count}") 

+

75 fail_message.append(f"Message: '{self.message}'") 

+

76 fail_message.append(f"Level: '{self.level}'") 

+

77 # only put in these if they were specified 

+

78 fail_message.append(f"Present: '{self.present}'") 

+

79 fail_message.append(f"Absent: '{self.absent}'") 

+

80 fail_message.append("") 

+

81 

+

82 if len(self.failed_matches) == 0: 

+

83 return fail_message 

+

84 fail_message.append( 

+

85 "Below is a list of the values for the selected extras for those failed matches.", # noqa: E501 

+

86 ) 

+

87 for match, count in sorted( 

+

88 self.failed_matches.items(), key=self._failed_sort_key 

+

89 ): 

+

90 msg = match 

+

91 if self.count and self.count != count: 

+

92 msg = ( 

+

93 match[:-1] 

+

94 + f', "Desired {self.count} call{"" if self.count == 1 else "s"}, actual {count} call{"" if count == 1 else "s"}"' # noqa: E501 

+

95 + match[-1:] 

+

96 ) 

+

97 fail_message.append(msg) 

+

98 return fail_message 

+

99 

+

100 if self.count != self.found: 

+

101 return [f"Found {self.found} matching messages, {self.count} were desired"] 

+

102 return [] 

+

103 

+

104 def _check_record_matches( 

+

105 self, 

+

106 record: logging.LogRecord, 

+

107 ) -> list[str]: 

+

108 differences = [] 

+

109 # `levelname` is often renamed. But, `levelno` shouldn't be touched as often 

+

110 # So, don't try to guess what the level name is, just use the levelno. 

+

111 level = { 

+

112 logging.DEBUG: "DEBUG", 

+

113 logging.INFO: "INFO", 

+

114 logging.WARNING: "WARNING", 

+

115 logging.ERROR: "ERROR", 

+

116 }[record.levelno] 

+

117 actual = { 

+

118 "level": level, 

+

119 "msg": record.msg, 

+

120 # The extras are already added as attributes, so this is the easiest way 

+

121 # to get them. There are more things in here, but that should be fine 

+

122 "extra": record.__dict__, 

+

123 } 

+

124 

+

125 if self.level != actual["level"]: 

+

126 differences.append( 

+

127 f"Desired level: {self.level}, actual level: {actual['level']}", 

+

128 ) 

+

129 # TODO @<crimsonknave>: Do a better string diff here # noqa: FIX002, TD003 

+

130 if self.message != actual["msg"]: 

+

131 differences.append( 

+

132 f"Desired message: '{self.message}', actual message: '{actual['msg']}'", 

+

133 ) 

+

134 

+

135 actual_keys = set(actual["extra"].keys()) 

+

136 desired_keys = set(self.present.keys()) 

+

137 

+

138 missing = desired_keys - actual_keys 

+

139 unwanted = set() 

+

140 if self.absent == AnnotatedLogMock.ALL: 

+

141 unwanted = actual_keys - AnnotatedLogMock.DEFAULT_LOG_KEYS 

+

142 elif isinstance(self.absent, set): 

+

143 unwanted = actual_keys & self.absent 

+

144 shared = desired_keys & actual_keys 

+

145 differences.extend([f"Missing key: `{key}`" for key in sorted(missing)]) 

+

146 

+

147 differences.extend([f"Unwanted key: `{key}`" for key in sorted(unwanted)]) 

+

148 

+

149 differences.extend( 

+

150 [ 

+

151 f"Extra `{key}` value is incorrect. Desired `{self.present[key]}` ({self.present[key].__class__}) , actual `{actual['extra'][key]}` ({actual['extra'][key].__class__})" # noqa: E501 

+

152 for key in sorted(shared) 

+

153 if self.present[key] != actual["extra"][key] 

+

154 ] 

+

155 ) 

+

156 return differences 

+

157 

+

158 

+

159class AnnotatedLogMock(logging.Handler): 

+

160 """Mock that captures logs and provides extra assertion logic.""" 

+

161 

+

162 ALL = "ALL" 

+

163 DEFAULT_LOG_KEYS = frozenset( 

+

164 [ 

+

165 "action", 

+

166 "annotated", 

+

167 "args", 

+

168 "created", 

+

169 "exc_info", 

+

170 "exc_text", 

+

171 "filename", 

+

172 "funcName", 

+

173 "levelname", 

+

174 "levelno", 

+

175 "lineno", 

+

176 "message", 

+

177 "module", 

+

178 "msecs", 

+

179 "msg", 

+

180 "name", 

+

181 "pathname", 

+

182 "process", 

+

183 "processName", 

+

184 "relativeCreated", 

+

185 "stack_info", 

+

186 "thread", 

+

187 "threadName", 

+

188 ] 

+

189 ) 

+

190 

+

191 def __init__(self, handler: logging.Handler) -> None: 

+

192 """Store the handler and initialize the messages and records lists.""" 

+

193 self.messages = [] 

+

194 self.records = [] 

+

195 self.handler = handler 

+

196 

+

197 def __getattr__(self, name: str) -> Any: # noqa: ANN401 

+

198 """Fall back to the real handler object.""" 

+

199 return getattr(self.handler, name) 

+

200 

+

201 def handle(self, record: logging.LogRecord) -> bool: 

+

202 """Wrap the real handle method, store the formatted message and log record.""" 

+

203 self.messages.append(self.handler.format(record)) 

+

204 self.records.append(record) 

+

205 return self.handler.handle(record) 

+

206 

+

207 def assert_logged( 

+

208 self, 

+

209 level: str | pychoir.core.Matcher | None = None, 

+

210 message: str | pychoir.core.Matcher | None = None, 

+

211 present: dict[str, Any] | None = None, 

+

212 absent: str | set[str] | list[str] | None = None, 

+

213 count: int | pychoir.core.Matcher | None = None, 

+

214 ) -> None: 

+

215 """Check if the mock received a log call that matches the arguments.""" 

+

216 if level is None: 

+

217 level = pychoir.existential.Anything() 

+

218 elif isinstance(level, str): 

+

219 level = level.upper() 

+

220 if message is None: 

+

221 message = pychoir.existential.Anything() 

+

222 if present is None: 

+

223 present = {} 

+

224 if absent is None: 

+

225 absent = [] 

+

226 if isinstance(absent, list): 

+

227 absent = set(absent) 

+

228 if isinstance(absent, str) and absent != "ALL": 

+

229 absent = {absent} 

+

230 if count is None: 

+

231 count = pychoir.numeric.IsPositive() 

+

232 __tracebackhide__ = True # pragma: no mutate 

+

233 assert_logged = AssertLogged(level, message, present, absent, count=count) 

+

234 assert_logged.check(self) 

+

235 

+

236 

+

237@pytest.fixture 

+

238def annotated_logger_object() -> logging.Logger: 

+

239 """Logger to wrap with the `annotated_logger_mock` fixture.""" 

+

240 return logging.getLogger("annotated_logger") 

+

241 

+

242 

+

243@pytest.fixture 

+

244def annotated_logger_mock(annotated_logger_object: logging.Logger) -> AnnotatedLogMock: 

+

245 """Fixture for a mock of the annotated logger.""" 

+

246 handler = annotated_logger_object.handlers[0] 

+

247 annotated_logger_object.removeHandler(handler) 

+

248 mock_handler = AnnotatedLogMock( 

+

249 handler=handler, 

+

250 ) 

+

251 

+

252 annotated_logger_object.addHandler(mock_handler) 

+

253 return mock_handler 

+
+ + + diff --git a/htmlcov/z_beb44c9891d1179a_plugins_py.html b/htmlcov/z_beb44c9891d1179a_plugins_py.html new file mode 100644 index 0000000..73fd646 --- /dev/null +++ b/htmlcov/z_beb44c9891d1179a_plugins_py.html @@ -0,0 +1,305 @@ + + + + + Coverage for annotated_logger/plugins.py: 100% + + + + + +
+
+

+ Coverage for annotated_logger/plugins.py: + 100% +

+ +

+ 92 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.9.1, + created at 2025-06-30 19:30 +0000 +

+ +
+
+
+

1from __future__ import annotations 

+

2 

+

3import contextlib 

+

4import logging 

+

5from typing import TYPE_CHECKING, Any, Callable 

+

6 

+

7from requests.exceptions import HTTPError 

+

8 

+

9from annotated_logger.filter import AnnotatedFilter 

+

10 

+

11if TYPE_CHECKING: # pragma: no cover 

+

12 from annotated_logger import AnnotatedAdapter 

+

13 

+

14 

+

15class BasePlugin: 

+

16 """Base class for plugins.""" 

+

17 

+

18 def filter(self, _record: logging.LogRecord) -> bool: 

+

19 """Determine if the record should be sent.""" 

+

20 return True 

+

21 

+

22 def uncaught_exception( 

+

23 self, exception: Exception, logger: AnnotatedAdapter 

+

24 ) -> AnnotatedAdapter: 

+

25 """Handle an uncaught excaption.""" 

+

26 if "success" not in logger.filter.annotations: 

+

27 logger.annotate(success=False) 

+

28 if "exception_title" not in logger.filter.annotations: 

+

29 logger.annotate(exception_title=str(exception)) 

+

30 return logger 

+

31 

+

32 

+

33class RuntimeAnnotationsPlugin(BasePlugin): 

+

34 """Plugin that sets annotations dynamically.""" 

+

35 

+

36 def __init__( 

+

37 self, runtime_annotations: dict[str, Callable[[logging.LogRecord], Any]] 

+

38 ) -> None: 

+

39 """Store the runtime annotations.""" 

+

40 self.runtime_annotations = runtime_annotations 

+

41 

+

42 def filter(self, record: logging.LogRecord) -> bool: 

+

43 """Add any configured runtime annotations.""" 

+

44 for key, function in self.runtime_annotations.items(): 

+

45 record.__dict__[key] = function(record) 

+

46 return True 

+

47 

+

48 

+

49class RequestsPlugin(BasePlugin): 

+

50 """Plugin for the requests library.""" 

+

51 

+

52 def uncaught_exception( 

+

53 self, exception: Exception, logger: AnnotatedAdapter 

+

54 ) -> AnnotatedAdapter: 

+

55 """Add the status code if possible.""" 

+

56 if isinstance(exception, HTTPError) and exception.response is not None: 

+

57 logger.annotate(status_code=exception.response.status_code) 

+

58 logger.annotate(exception_title=exception.response.reason) 

+

59 return logger 

+

60 

+

61 

+

62class RenamerPlugin(BasePlugin): 

+

63 """Plugin that prevents name collisions.""" 

+

64 

+

65 class FieldNotPresentError(Exception): 

+

66 """Exception for a field that is supposed to be renamed, but is not present.""" 

+

67 

+

68 def __init__(self, *, strict: bool = False, **kwargs: str) -> None: 

+

69 """Store the list of names to rename and pre/post fixs.""" 

+

70 self.targets = kwargs 

+

71 self.strict = strict 

+

72 

+

73 def filter(self, record: logging.LogRecord) -> bool: 

+

74 """Adjust the name of any fields that match a provided list if they exist.""" 

+

75 for new, old in self.targets.items(): 

+

76 if old in record.__dict__: 

+

77 record.__dict__[new] = record.__dict__[old] 

+

78 del record.__dict__[old] 

+

79 elif self.strict: 

+

80 raise RenamerPlugin.FieldNotPresentError(old) 

+

81 return True 

+

82 

+

83 

+

84class RemoverPlugin(BasePlugin): 

+

85 """Plugin that removed fields.""" 

+

86 

+

87 def __init__(self, targets: list[str] | str) -> None: 

+

88 """Store the list of names to remove.""" 

+

89 if isinstance(targets, str): 

+

90 targets = [targets] 

+

91 self.targets = targets 

+

92 

+

93 def filter(self, record: logging.LogRecord) -> bool: 

+

94 """Remove the specified fields.""" 

+

95 for target in self.targets: 

+

96 with contextlib.suppress(KeyError): 

+

97 del record.__dict__[target] 

+

98 return True 

+

99 

+

100 

+

101class NameAdjusterPlugin(BasePlugin): 

+

102 """Plugin that prevents name collisions with splunk field names.""" 

+

103 

+

104 def __init__(self, names: list[str], prefix: str = "", postfix: str = "") -> None: 

+

105 """Store the list of names to rename and pre/post fixs.""" 

+

106 self.names = names 

+

107 self.prefix = prefix 

+

108 self.postfix = postfix 

+

109 

+

110 def filter(self, record: logging.LogRecord) -> bool: 

+

111 """Adjust the name of any fields that match a provided list.""" 

+

112 for name in self.names: 

+

113 if name in record.__dict__: 

+

114 value = record.__dict__[name] 

+

115 del record.__dict__[name] 

+

116 record.__dict__[f"{self.prefix}{name}{self.postfix}"] = value 

+

117 return True 

+

118 

+

119 

+

120class NestedRemoverPlugin(BasePlugin): 

+

121 """Plugin that removes nested fields.""" 

+

122 

+

123 def __init__(self, keys_to_remove: list[str]) -> None: 

+

124 """Store the list of keys to remove.""" 

+

125 self.keys_to_remove = keys_to_remove 

+

126 

+

127 def filter(self, record: logging.LogRecord) -> bool: 

+

128 """Remove the specified fields.""" 

+

129 

+

130 def delete_keys_nested( 

+

131 target: dict, # pyright: ignore[reportMissingTypeArgument] 

+

132 keys_to_remove: list, # pyright: ignore[reportMissingTypeArgument] 

+

133 ) -> dict: # pyright: ignore[reportMissingTypeArgument] 

+

134 for key in keys_to_remove: 

+

135 with contextlib.suppress(KeyError): 

+

136 del target[key] 

+

137 for value in target.values(): 

+

138 if isinstance(value, dict): 

+

139 delete_keys_nested(value, keys_to_remove) 

+

140 return target 

+

141 

+

142 record.__dict__ = delete_keys_nested(record.__dict__, self.keys_to_remove) 

+

143 return True 

+

144 

+

145 

+

146class GitHubActionsPlugin(BasePlugin): 

+

147 """Plugin that will format log messages for actions annotations.""" 

+

148 

+

149 def __init__(self, annotation_level: int) -> None: 

+

150 """Save the annotation level.""" 

+

151 self.annotation_level = annotation_level 

+

152 self.base_attributes = logging.makeLogRecord({}).__dict__ # pragma: no mutate 

+

153 self.attributes_to_exclude = {"annotated"} 

+

154 

+

155 def filter(self, record: logging.LogRecord) -> bool: 

+

156 """Set the actions command to be an annotation if desired.""" 

+

157 if record.levelno < self.annotation_level: 

+

158 return False 

+

159 

+

160 added_attributes = { 

+

161 k: v 

+

162 for k, v in record.__dict__.items() 

+

163 if k not in self.base_attributes and k not in self.attributes_to_exclude 

+

164 } 

+

165 record.added_attributes = added_attributes 

+

166 name = record.levelname.lower() 

+

167 if name == "info": # pragma: no cover 

+

168 name = "notice" 

+

169 record.github_annotation = f"{name}::" 

+

170 

+

171 return True 

+

172 

+

173 def logging_config(self) -> dict[str, dict[str, object]]: 

+

174 """Generate the default logging config for the plugin.""" 

+

175 return { 

+

176 "handlers": { 

+

177 "actions_handler": { 

+

178 "class": "logging.StreamHandler", 

+

179 "filters": ["actions_filter"], 

+

180 "formatter": "actions_formatter", 

+

181 }, 

+

182 }, 

+

183 "filters": { 

+

184 "actions_filter": { 

+

185 "()": AnnotatedFilter, 

+

186 "plugins": [ 

+

187 BasePlugin(), 

+

188 self, 

+

189 ], 

+

190 }, 

+

191 }, 

+

192 "formatters": { 

+

193 "actions_formatter": { 

+

194 "format": "{github_annotation} {message} - {added_attributes}", 

+

195 "style": "{", 

+

196 }, 

+

197 }, 

+

198 "loggers": { 

+

199 "annotated_logger.actions": { 

+

200 "level": "DEBUG", 

+

201 "handlers": [ 

+

202 # This is from the default logging config 

+

203 # "annotated_handler", 

+

204 "actions_handler", 

+

205 ], 

+

206 }, 

+

207 }, 

+

208 } 

+
+ + + diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 1ace559..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,116 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "annotated-logger" -dynamic = ["version"] -description = "A decorator that logs the start and end of a function as well as providing an optional logger with annotations" -readme = "README.md" -license = "MIT" -requires-python = ">=3.6" -authors = [ - { name = "Vuln Mgmt Eng", email = "security+vulnmgmteng@github.com" }, -] -keywords = [ - "decorator", - "logging", - "annotation", -] -dependencies = [ - "makefun", - "python-json-logger>=3.1.0", - "requests", - # The mock makes use of this, but we really should separate the mock out into it's own package - # That would allow the mock to be included in dev, not in prod - "pychoir", -] - -[project.entry-points.pytest11] -annotated_logger = "annotated_logger.mocks" - -[project.urls] -Homepage = "https://github.com/github/annotated-logger" - -[tool.hatch.version] -path = "annotated_logger/__init__.py" - -[tool.hatch.build.targets.sdist] -include = [ - "/annotated_logger", -] - -[tool.hatch.env] -requires = [ - "hatch-pip-compile" -] - -[tool.hatch.envs.default] -type = "pip-compile" -installer = "uv" -pip-compile-resolver = "uv" - -[tool.hatch.envs.dev] -type = "pip-compile" -installer = "uv" -pip-compile-resolver = "uv" -dependencies = [ - "coverage", - "mutmut", - "pre-commit", - "pyright", - "pytest", - "pytest-cov", - "pytest-freezer", - "pytest-github-actions-annotate-failures", - "pytest-mock", - "pytest-randomly", - "requests-mock", - "ruff", - "typing_extensions", -] - -[tool.hatch.envs.dev.scripts] -typing = "pyright" -test = "pytest" -lint = "ruff check" - -[tool.coverage.report] -exclude_also = ["@overload"] -fail_under = 100 - -[tool.mutmut] -paths_to_mutate = "annotated_logger/" -runner = "script/mutmut_runner" -use_coverage = true - -[tool.pyright] -include = ["annotated_logger", "example", "test"] -reportMissingTypeArgument = true -# venvPath = "." -# venv = ".venv" - -[tool.pytest.ini_options] -# -p no:annotated_logger disables the plugin so that we can request it conftest.py -# to get coverage correctly, users will not have to do this. -# See https://github.com/pytest-dev/pytest/issues/935 -addopts = "--cov=annotated_logger --cov=example --cov-report html --cov-report term -p no:annotated_logger" -filterwarnings = [ - "error", -] - -[tool.ruff] -lint.select = ["ALL"] -lint.ignore = [ - "D100", "D104", - # Disabled as they conflict with the formatter: - "W191", "E111", "E114", "E117", "D206", "D300", "Q000", "Q001", "Q002", "Q003", "COM812", "COM819", "ISC001", "ISC002", - # Conflicts with other rule, choosing one - "D203", "D213", -] - -[tool.ruff.lint.per-file-ignores] -"test/*" = ["E501", "D10", "ANN", "S101", "PLR2004"] - -[tool.ruff.lint.flake8-annotations] -allow-star-arg-any = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 965f26d..0000000 --- a/requirements.txt +++ /dev/null @@ -1,25 +0,0 @@ -# -# This file is autogenerated by hatch-pip-compile with Python 3.12 -# -# - makefun -# - pychoir -# - python-json-logger>=3.1.0 -# - requests -# - -certifi==2024.12.14 - # via requests -charset-normalizer==3.4.1 - # via requests -idna==3.10 - # via requests -makefun==1.15.6 - # via hatch.envs.default -pychoir==0.0.27 - # via hatch.envs.default -python-json-logger==3.2.1 - # via hatch.envs.default -requests==2.32.4 - # via hatch.envs.default -urllib3==2.3.0 - # via requests diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt deleted file mode 100644 index af0b26d..0000000 --- a/requirements/requirements-dev.txt +++ /dev/null @@ -1,147 +0,0 @@ -# -# This file is autogenerated by hatch-pip-compile with Python 3.10 -# -# - coverage -# - mutmut -# - pre-commit -# - pyright -# - pytest -# - pytest-cov -# - pytest-freezer -# - pytest-github-actions-annotate-failures -# - pytest-mock -# - pytest-randomly -# - requests-mock -# - ruff -# - typing_extensions -# - makefun -# - pychoir -# - python-json-logger>=3.1.0 -# - requests -# - -certifi==2024.12.14 - # via requests -cfgv==3.4.0 - # via pre-commit -charset-normalizer==3.4.1 - # via requests -click==8.1.8 - # via mutmut -coverage==7.6.10 - # via - # hatch.envs.dev - # pytest-cov -distlib==0.3.9 - # via virtualenv -exceptiongroup==1.2.2 - # via pytest -filelock==3.16.1 - # via virtualenv -freezegun==1.5.1 - # via pytest-freezer -identify==2.6.5 - # via pre-commit -idna==3.10 - # via requests -iniconfig==2.0.0 - # via pytest -junit-xml==1.8 - # via mutmut -linkify-it-py==2.0.3 - # via markdown-it-py -makefun==1.15.6 - # via hatch.envs.dev -markdown-it-py==3.0.0 - # via - # mdit-py-plugins - # rich - # textual -mdit-py-plugins==0.4.2 - # via markdown-it-py -mdurl==0.1.2 - # via markdown-it-py -mutmut==3.2.2 - # via hatch.envs.dev -nodeenv==1.9.1 - # via - # pre-commit - # pyright -packaging==24.2 - # via pytest -parso==0.8.4 - # via mutmut -platformdirs==4.3.6 - # via - # textual - # virtualenv -pluggy==1.5.0 - # via pytest -pre-commit==4.0.1 - # via hatch.envs.dev -pychoir==0.0.27 - # via hatch.envs.dev -pygments==2.19.1 - # via rich -pyright==1.1.391 - # via hatch.envs.dev -pytest==8.3.4 - # via - # hatch.envs.dev - # pytest-cov - # pytest-freezer - # pytest-github-actions-annotate-failures - # pytest-mock - # pytest-randomly -pytest-cov==6.0.0 - # via hatch.envs.dev -pytest-freezer==0.4.9 - # via hatch.envs.dev -pytest-github-actions-annotate-failures==0.2.0 - # via hatch.envs.dev -pytest-mock==3.14.0 - # via hatch.envs.dev -pytest-randomly==3.16.0 - # via hatch.envs.dev -python-dateutil==2.9.0.post0 - # via freezegun -python-json-logger==3.2.1 - # via hatch.envs.dev -pyyaml==6.0.2 - # via pre-commit -requests==2.32.4 - # via - # hatch.envs.dev - # requests-mock -requests-mock==1.12.1 - # via hatch.envs.dev -rich==13.9.4 - # via textual -ruff==0.8.6 - # via hatch.envs.dev -setproctitle==1.3.4 - # via mutmut -six==1.17.0 - # via - # junit-xml - # python-dateutil -textual==1.0.0 - # via mutmut -toml==0.10.2 - # via mutmut -tomli==2.2.1 - # via - # coverage - # pytest -typing-extensions==4.12.2 - # via - # hatch.envs.dev - # pyright - # rich - # textual -uc-micro-py==1.0.3 - # via linkify-it-py -urllib3==2.3.0 - # via requests -virtualenv==20.28.1 - # via pre-commit diff --git a/script/bootstrap b/script/bootstrap deleted file mode 100755 index 013e0ff..0000000 --- a/script/bootstrap +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -x -set -e - -DEV_FLAG="--dev" script/bootstrap_python - -hatch run dev:pre-commit install diff --git a/script/bootstrap_python b/script/bootstrap_python deleted file mode 100755 index 99298b5..0000000 --- a/script/bootstrap_python +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -x -set -e - -cd "$(dirname $0)/.." -export PIPENV_VENV_IN_PROJECT="enabled" -if ! command -v hatch &>/dev/null; then - pip3 install hatch -fi - -hatch env prune -hatch env create dev diff --git a/script/lint b/script/lint deleted file mode 100755 index c823570..0000000 --- a/script/lint +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -e - -cd "$(dirname "$0")/.." -ROOT=$(pwd) -source $ROOT/script/_cibuild.lib - -begin_fold "Linting..." -hatch run dev:ruff check -end_fold diff --git a/script/mutate b/script/mutate deleted file mode 100644 index 4d8993e..0000000 --- a/script/mutate +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -set -e - -cd "$(dirname "$0")/.." -ROOT=$(pwd) -source $ROOT/script/_cibuild.lib - -begin_fold "Mutation Testing..." - -hatch run dev:mutmut run --CI --simple-output - -hatch run dev:junitxml - -end_fold diff --git a/script/mutmut_runner b/script/mutmut_runner deleted file mode 100755 index b7310af..0000000 --- a/script/mutmut_runner +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -echo "Running Ruff" -ruff check -echo "Running Pytest" -pytest -x -echo "Running Pyright" -pyright diff --git a/script/sort_wordlist b/script/sort_wordlist deleted file mode 100755 index 563d063..0000000 --- a/script/sort_wordlist +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -PROJECT_DIR="$( dirname "$SCRIPT_DIR}" )"; -cd $PROJECT_DIR - -sort -uf -o wordlist.txt wordlist.txt -echo "Sorted wordlist.txt!" diff --git a/script/test b/script/test deleted file mode 100755 index 356b4ae..0000000 --- a/script/test +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set -e - -cd "$(dirname "$0")/.." -ROOT=$(pwd) -source $ROOT/script/_cibuild.lib - -begin_fold "Testing..." - -# Move the coveragerc file specific to the test type to .coveragerc -# This allows us to ignore coverage for logging bits in acceptance -if [ -f ".coveragerc.$1" ] -then - cp ".coveragerc.$1" .coveragerc -fi - -hatch run dev:pytest --verbose \ ---inline_errors=$CI_MODE \ ---cov-fail-under=$COV_MIN \ -test \ --m "$1" - -end_fold diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index 8efdf25..0000000 --- a/test/conftest.py +++ /dev/null @@ -1,52 +0,0 @@ -from __future__ import annotations - -import importlib - -import pytest - -import example.actions -import example.api -import example.calculator -import example.default -import example.logging_config - -# Due to the lack of config in pytest plugin ordering we have to manually add it here -# so that we get coverage on the code correctly. Users will not need to. -# See https://github.com/pytest-dev/pytest/issues/935 -pytest_plugins = ["annotated_logger.mocks"] - - -@pytest.fixture -def fail_mock(mocker): - return mocker.patch("annotated_logger.mocks.pytest.fail") - - -# These fixtures are used in place of importing the classes with -# `from example.api import ApiClient` -# They force the relevant module to be reloaded, which will reset the -# logging config, as it gets clobbered by the most recently imported module -# This is a more complete solution than using `pytest-forked` which only -# fixed the issue if the test file didn't import more than one that conflicted -@pytest.fixture -def _reload_api(): - importlib.reload(example.api) - - -@pytest.fixture -def _reload_calculator(): - importlib.reload(example.calculator) - - -@pytest.fixture -def _reload_default(): - importlib.reload(example.default) - - -@pytest.fixture -def _reload_actions(): - importlib.reload(example.actions) - - -@pytest.fixture -def _reload_logging_config(): - importlib.reload(example.logging_config) diff --git a/test/demo.py b/test/demo.py deleted file mode 100644 index 7a72f42..0000000 --- a/test/demo.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging - -from annotated_logger import AnnotatedLogger -from annotated_logger.plugins import RuntimeAnnotationsPlugin - - -def runtime(_record: logging.LogRecord): - return "this function is called every time" - - -annotated_logger = AnnotatedLogger( - annotations={"extra": "new data"}, - plugins=[RuntimeAnnotationsPlugin({"runtime": runtime})], -) - -annotate_logs = annotated_logger.annotate_logs - - -@annotate_logs(_typing_self=False, _typing_requested=True) -def function_without_parameters(annotated_logger): - annotated_logger.info("This is my message") - return True diff --git a/test/test_decorator.py b/test/test_decorator.py deleted file mode 100644 index fcceef3..0000000 --- a/test/test_decorator.py +++ /dev/null @@ -1,620 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest -from pychoir.strings import StartsWith - -import example.api -import example.calculator -import example.default -import test.demo -from annotated_logger.plugins import RuntimeAnnotationsPlugin - -if TYPE_CHECKING: - from annotated_logger.mocks import AnnotatedLogMock - - -@pytest.mark.usefixtures("_reload_calculator") -class TestCalculatorExample: - # Test logged exceptions with Calculator.divide() - def test_logged_exception(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 0) - with pytest.raises(ZeroDivisionError): - calc.divide() - - extras = { - "action": "example.calculator:Calculator.divide", - "extra": "new data", - "runtime": "this function is called every time", - "exception_title": "division by zero", - "success": False, - } - - annotated_logger_mock.assert_logged( - "error", "Uncaught Exception in logged function", extras - ) - - # Test logged attributes with Calculator.add() - def test_success_info_false(self, annotated_logger_mock): - calc = example.calculator.Calculator(6, 7) - calc.add() - annotated_logger_mock.assert_logged( - "DEBUG", - "success", - { - "action": "example.calculator:Calculator.add", - "extra": "new data", - "runtime": "this function is called every time", - "first": 6, - "second": 7, - "run_time": "0.0", - "success": True, - }, - ) - - def test_annotate_logger(self, annotated_logger_mock): - calc = example.calculator.Calculator(6, 7) - calc.add() - annotated_logger_mock.assert_logged( - "DEBUG", - "success", - { - "action": "example.calculator:Calculator.add", - "annotated": True, - "extra": "new data", - "runtime": "this function is called every time", - "first": 6, - "second": 7, - "run_time": "0.0", - "success": True, - }, - ) - # Ensure that the logs in multiply don't have the annotate_logger from add - calc.multiply(1, 2) - annotated_logger_mock.assert_logged( - "DEBUG", - "success", - present={ - "action": "example.calculator:Calculator.multiply", - "extra": "new data", - "runtime": "this function is called every time", - "first": 1, - "second": 2, - "run_time": "0.0", - "success": True, - }, - absent=["foo"], - ) - - def test_can_provide_annotated_logger(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 5) - answer = calc.power(2, 3) - assert answer == 8 - annotated_logger_mock.assert_logged( - "INFO", - "success", - { - "action": "example.calculator:Calculator.power", - "power_overwhelming": True, - }, - absent=["first", "second", "subaction"], - count=1, - ) - annotated_logger_mock.assert_logged( - "DEBUG", - "success", - { - "action": "example.calculator:Calculator.power", - "power_overwhelming": True, - "first": 2, - "second": 2, - "subaction": "example.calculator:Calculator.multiply2", - }, - count=1, - ) - annotated_logger_mock.assert_logged( - "DEBUG", - "success", - { - "action": "example.calculator:Calculator.power", - "power_overwhelming": True, - "first": 4, - "second": 2, - "subaction": "example.calculator:Calculator.multiply2", - }, - count=1, - ) - - # Test logger info call with Calculator.add() - def test_info(self, annotated_logger_mock): - calc = example.calculator.Calculator(8, 9) - calc.add() - # This also tests the `annotate_logger` method - annotated_logger_mock.assert_logged( - "INFO", - "This message will have 'other' as well as 'first' from the annotation above.", - { - "action": "example.calculator:Calculator.add", - "first": 8, - "second": 9, - "other": "value", - "extra": "new data", - "runtime": "this function is called every time", - }, - ) - annotated_logger_mock.assert_logged( - "INFO", - "This message will have the 'first' annotation and the defaults, but not the 'other'", - { - "action": "example.calculator:Calculator.add", - "first": 8, - "second": 9, - "extra": "new data", - "runtime": "this function is called every time", - }, - ) - - # Test logger warn call with Calculator.divide() - def test_warn(self, annotated_logger_mock): - calc = example.calculator.Calculator(10, 11) - calc.divide() - - annotated_logger_mock.assert_logged( - "WARNING", - "If you divide by zero you'll create a singularity in the fabric of space-time!", - { - "action": "example.calculator:Calculator.divide", - "extra": "new data", - "runtime": "this function is called every time", - }, - ) - - # Test logger error call with Calculator.add() with a bad value - def test_error(self, annotated_logger_mock): - calc = example.calculator.Calculator(None, 2) # pyright: ignore[reportArgumentType] - calc.add() - annotated_logger_mock.assert_logged( - "ERROR", - "Must have a first value!", - { - "action": "example.calculator:Calculator.add", - "extra": "new data", - "runtime": "this function is called every time", - "first": None, - "second": 2, - }, - ) - - # Test logger exception call with Calculator.inverse() - def test_exception(self, annotated_logger_mock): - example.calculator.Calculator(1, 11).inverse(0) - annotated_logger_mock.assert_logged( - "ERROR", - "Cannot divide by zero!", - { - "action": "example.calculator:Calculator.inverse", - "extra": "new data", - "runtime": "this function is called every time", - }, - ) - - # Test logger debug call with Calculator.subtract() - def test_debug(self, annotated_logger_mock): - calc = example.calculator.Calculator(12, 13) - calc.subtract() - annotated_logger_mock.assert_logged( - "DEBUG", - "Order does matter when subtracting", - { - "action": "example.calculator:Calculator.subtract", - "extra": "new data", - "runtime": "this function is called every time", - }, - ) - - def test_runtime_not_cached(self, annotated_logger_mock, mocker): - runtime_mock = mocker.Mock(name="runtime_not_cached") - runtime_mock.side_effect = ["first", "second", "third", "fourth"] - plugin = next( - plugin - for plugin in example.calculator.annotated_logger.plugins - if isinstance(plugin, RuntimeAnnotationsPlugin) - ) - - runtime_annotations = plugin.runtime_annotations - plugin.runtime_annotations = {"runtime": runtime_mock} - # Need to use full path as that's what's reloaded. The other tests don't need - # to as they're not mocking the runtime annotations - calc = example.calculator.Calculator(12, 13) - calc.subtract() - annotated_logger_mock.assert_logged( - "DEBUG", - "start", - { - "action": "example.calculator:Calculator.subtract", - "extra": "new data", - "runtime": "first", - }, - ) - annotated_logger_mock.assert_logged( - "DEBUG", - "Order does matter when subtracting", - { - "action": "example.calculator:Calculator.subtract", - "extra": "new data", - "runtime": "second", - }, - ) - plugin.runtime_annotations = runtime_annotations - - def test_raises_type_error_with_too_few_args(self): - calc = example.calculator.Calculator(12, 13) - with pytest.raises(TypeError): - calc.multiply() # pyright: ignore[reportCallIssue] - - def test_raises_type_error_with_too_many_args(self): - calc = example.calculator.Calculator(12, 13) - with pytest.raises( - TypeError, - match=r"^Calculator.subtract\(\) takes 1 positional argument but 2 were given$", - ): - calc.subtract(1) # pyright: ignore[reportCallIssue] - with pytest.raises( - TypeError, - match=r"^Calculator\.subtract\(\) takes 1 positional argument but 3 were given$", - ): - calc.subtract(1, 2) # pyright: ignore[reportCallIssue] - - # Test list instance in logger with Calculator.pemdas_example() - def test_list(self, annotated_logger_mock): - calc = example.calculator.Calculator(12, 1) - calc.pemdas_example() - annotated_logger_mock.assert_logged( - "INFO", - "success", - { - "action": "example.calculator:Calculator.pemdas_example", - "extra": "new data", - "runtime": "this function is called every time", - "run_time": "0.0", - "count": 2, - "success": True, - }, - ) - - # Test json formatter with Calculator.is_odd() - def test_formatter(self, annotated_logger_mock): - example.calculator.Calculator(1, 9).is_odd(14) - annotated_logger_mock.assert_logged( - "INFO", - "success", - { - "action": "example.calculator:Calculator.is_odd", - "extra": "new data", - "runtime": "this function is called every time", - "run_time": "0.0", - "success": True, - }, - ) - - # Test logging via a log object created with logging.getflogger - def test_getlogger_logger(self): - example.calculator.Calculator(1, 10).is_odd(14) - - def test_function_without_a_parameter(self, annotated_logger_mock): - test.demo.function_without_parameters() - - annotated_logger_mock.assert_logged( - "INFO", - "success", - { - "action": "test.demo:function_without_parameters", - "extra": "new data", - "name": StartsWith("annotated_logger"), - "runtime": "this function is called every time", - "run_time": "0.0", - "success": True, - }, - ) - annotated_logger_mock.assert_logged( - "INFO", - "This is my message", - { - "action": "test.demo:function_without_parameters", - "extra": "new data", - "runtime": "this function is called every time", - }, - ) - - def test_iterator(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 0) - value = calc.factorial(5) - assert value == 120 - for i in [1, 2, 3, 4, 5]: - annotated_logger_mock.assert_logged( - "info", - "next", - present={ - "value": i, - "extra": "new data", - "runtime": "this function is called every time", - "iterator": "factorial numbers", - }, - ) - - @pytest.mark.parametrize( - "level", ["debug", "info", "warning", "error", "exception"] - ) - def test_sensitive_iterator(self, annotated_logger_mock, level): - calc = example.calculator.Calculator(1, 0) - value = calc.sensitive_factorial(5, level) - five_factorial = 120 - assert value == five_factorial - if level == "exception": - level = "error" - annotated_logger_mock.assert_logged( - level, - "next", - present={ - "extra": "new data", - "runtime": "this function is called every time", - }, - absent=["value"], - count=5, - ) - - def test_logs_len_if_it_exists(self, annotated_logger_mock): - class Weird: - def __len__(self): - return 999 - - @example.calculator.annotated_logger.annotate_logs(_typing_self=False) - def test_me(): - return Weird() - - test_me() - annotated_logger_mock.assert_logged( - "INFO", - "success", - present={"count": 999}, - ) - - def test_classmethod(self, annotated_logger_mock): - assert example.calculator.Calculator.is_math_cool() is True - annotated_logger_mock.assert_logged( - "INFO", "What a silly question!", absent=["sane", "source"] - ) - annotated_logger_mock.assert_logged( - "INFO", - "Checking sanity", - present={ - "sane": True, - "source": "is_math_cool", - }, - ) - - def test_pre_call(self, annotated_logger_mock): - calc = example.calculator.Calculator(5, 11) - calc.divide() - annotated_logger_mock.assert_logged( - "DEBUG", "start", present={"will_crash": False} - ) - - def test_post_call(self, annotated_logger_mock): - calc = example.calculator.Calculator(5, 11) - calc.divide() - annotated_logger_mock.assert_logged( - "info", - "Prediction result", - present={"will_crash": False, "success": True, "result": True}, - ) - - def test_post_call_exception(self, annotated_logger_mock): - calc = example.calculator.Calculator(5, 0) - with pytest.raises(ZeroDivisionError): - calc.divide() - annotated_logger_mock.assert_logged( - "info", - "Prediction result", - present={"will_crash": True, "success": False, "result": True}, - ) - - def test_post_call_boom(self, annotated_logger_mock): - calc = example.calculator.Calculator(5, 0) - calc.boom = True - with pytest.raises(example.calculator.BoomError): - calc.multiply(1, 2) - annotated_logger_mock.assert_logged( - "warning", - "boom", - count=1, - ) - annotated_logger_mock.assert_logged( - "error", - "Post call failed", - present={ - "action": "example.calculator:Calculator.multiply", - "success": False, - "will_crash": False, - }, - ) - annotated_logger_mock.assert_logged( - "error", - "Uncaught Exception in logged function", - present={ - "action": "example.calculator:Calculator.multiply", - "success": False, - "will_crash": False, - }, - ) - - def test_pre_call_boom(self, annotated_logger_mock): - calc = example.calculator.Calculator(5, 11) - del calc.second - with pytest.raises(AttributeError): - calc.divide() - annotated_logger_mock.assert_logged( - "error", - "Uncaught Exception in logged function", - present={ - "action": "example.calculator:Calculator.divide", - "success": False, - }, - ) - - def test_args_kwargs_splat(self, annotated_logger_mock: AnnotatedLogMock): - default = example.default.DefaultExample() - default.var_args_and_kwargs( - "first", "arg0", "first_arg", kwarg1="kwarg1", kwarg2="second_kwarg" - ) - annotated_logger_mock.assert_logged( - "info", - "success", - present={ - "arg0": "arg0", - "arg1": "first_arg", - "kwarg1": "kwarg1", - "kwarg2": "second_kwarg", - }, - count=1, - ) - - def test_positional_only(self, annotated_logger_mock: AnnotatedLogMock): - default = example.default.DefaultExample() - default.positional_only("first", _second="second") - annotated_logger_mock.assert_logged( - "info", - "success", - present={"first": "first", "second": "second"}, - count=1, - ) - - def test_args_kwargs_splat_provided(self, annotated_logger_mock: AnnotatedLogMock): - default = example.default.DefaultExample() - - default.var_args_and_kwargs_provided_outer( - "first", "arg0", "first_arg", kwarg1="kwarg1", kwarg2="second_kwarg" - ) - annotated_logger_mock.assert_logged( - "info", - "success", - present={ - "outer": True, - "subaction": "example.default:DefaultExample.var_args_and_kwargs_provided", - "arg0": "arg0", - "arg1": "first_arg", - "kwarg1": "kwarg1", - "kwarg2": "second_kwarg", - }, - count=1, - ) - - -@pytest.mark.usefixtures("_reload_default") -class TestDefaultExample: - def test_no_annotations(self, annotated_logger_mock): - default = example.default.DefaultExample() - default.foo() - annotated_logger_mock.assert_logged( - "info", - "foo", - absent=annotated_logger_mock.ALL, - count=1, - ) - - def test_args_splat(self, annotated_logger_mock: AnnotatedLogMock): - default = example.default.DefaultExample() - default.var_args("first", "arg0", "first_arg") - annotated_logger_mock.assert_logged( - "info", - "success", - present={"arg0": "arg0", "arg1": "first_arg"}, - count=1, - ) - - def test_kwargs_splat(self, annotated_logger_mock: AnnotatedLogMock): - default = example.default.DefaultExample() - default.var_kwargs("first", kwarg1="kwarg1", kwarg2="second_kwarg") - annotated_logger_mock.assert_logged( - "info", - "success", - present={"kwarg1": "kwarg1", "kwarg2": "second_kwarg"}, - count=1, - ) - - -@pytest.mark.usefixtures("_reload_api") -class TestApiExample: - def test_decorated_class(self, annotated_logger_mock: AnnotatedLogMock): - api = example.api.ApiClient() - annotated_logger_mock.assert_logged( - "DEBUG", - "init", - present={ - "class": "example.api:ApiClient", - }, - absent="action", - ) - api.check() - annotated_logger_mock.assert_logged( - "INFO", - "Check passed", - present={ - "class": "example.api:ApiClient", - "action": "example.api:ApiClient.check", - "valid": True, - "lasting": "forever", - }, - ) - api.check_again() - annotated_logger_mock.assert_logged( - "INFO", - "Check passed", - present={ - "class": "example.api:ApiClient", - "action": "example.api:ApiClient.check_again", - "valid": True, - "lasting": "forever", - }, - ) - api.prepare() - annotated_logger_mock.assert_logged( - "INFO", - "Preparation complete", - present={ - "class": "example.api:ApiClient", - "action": "example.api:ApiClient.prepare", - "prepared": True, - "lasting": "forever", - }, - absent=["valid", "args_length"], - ) - - def test_args_kwargs_splat_provided_not_instance( - self, annotated_logger_mock: AnnotatedLogMock - ): - example.default.var_args_and_kwargs_provided_outer( - "first", "arg0", "first_arg", kwarg1="kwarg1", kwarg2="second_kwarg" - ) - annotated_logger_mock.assert_logged( - "info", - "success", - present={ - "outer": True, - "subaction": "example.default:var_args_and_kwargs_provided", - "arg0": "arg0", - "arg1": "first_arg", - "kwarg1": "kwarg1", - "kwarg2": "second_kwarg", - }, - count=1, - ) - - -class TestNonClassBased: - def test_annotated_logger_must_be_first(self): - with pytest.raises( - TypeError, match="^annotated_logger must be the first argument$" - ): - import example.invalid_order # noqa: F401 diff --git a/test/test_logging_config.py b/test/test_logging_config.py deleted file mode 100644 index f9da807..0000000 --- a/test/test_logging_config.py +++ /dev/null @@ -1,191 +0,0 @@ -import logging - -import pytest - -import example.logging_config -from annotated_logger.mocks import AnnotatedLogMock - - -@pytest.fixture -def annotated_logger_object(): - return logging.getLogger("annotated_logger.logging_config") - - -@pytest.mark.usefixtures("_reload_logging_config") -class TestLoggingConfig: - @pytest.mark.parametrize( - "annotated_logger_object", - [logging.getLogger("annotated_logger.logging_config.logger")], - ) - def test_base_logging(self, annotated_logger_mock): - example.logging_config.make_some_logs() - for level in ["debug", "info", "warning", "error"]: - annotated_logger_mock.assert_logged( - level, - f"this is {level}", - present={ - "decorated": False, - "config_based_filter": True, - "class_based_filter": True, - "lvl": level.upper(), - }, - absent=["weird", "hostname"], - ) - - def test_annotated_logging(self, annotated_logger_mock: AnnotatedLogMock): - example.logging_config.make_some_annotated_logs() - annotated_logger_mock.assert_logged( - "DEBUG", - "start", - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - }, - ) - annotated_logger_mock.assert_logged( - "DEBUG", - "this is debug", - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - }, - ) - - @pytest.mark.parametrize( - "annotated_logger_object", - [logging.getLogger("annotated_logger.logging_config_weird")], - ) - def test_weird_logging(self, annotated_logger_mock: AnnotatedLogMock): - example.logging_config.make_some_weird_logs() - annotated_logger_mock.assert_logged( - "DEBUG", - "this is debug", - present={ - "weird": True, - "annotated": True, - }, - absent="hostname", - # The weird logging level is info - count=0, - ) - annotated_logger_mock.assert_logged( - "INFO", - "this is info", - present={ - "weird": True, - "annotated": True, - }, - absent="hostname", - ) - annotated_logger_mock.assert_logged( - "WARNING", - "this is warning", - present={ - "weird": True, - "annotated": True, - }, - absent="hostname", - ) - annotated_logger_mock.assert_logged( - "ERROR", - "this is error", - present={ - "weird": True, - "annotated": True, - }, - absent="hostname", - ) - - @pytest.mark.parametrize( - "annotated_logger_object", - [logging.getLogger("annotated_logger.logging_config.long")], - ) - def test_really_long_message(self, annotated_logger_mock: AnnotatedLogMock): - example.logging_config.log_really_long_message() - annotated_logger_mock.assert_logged( - "INFO", - "1" * 200, - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - "message_part": 1, - "message_parts": 3, - "split": True, - "split_complete": False, - }, - ) - annotated_logger_mock.assert_logged( - "INFO", - "2" * 200, - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - "message_part": 2, - "message_parts": 3, - "split": True, - "split_complete": False, - }, - ) - annotated_logger_mock.assert_logged( - "INFO", - "3333", - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - "message_part": 3, - "message_parts": 3, - "split": True, - "split_complete": True, - }, - ) - annotated_logger_mock.assert_logged( - "INFO", - "4" * 200, - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - }, - absent=["split", "split_complete", "message_parts", "message_part"], - ) - annotated_logger_mock.assert_logged( - "INFO", - "5" * 200, - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - "message_part": 1, - "message_parts": 2, - "split": True, - "split_complete": False, - }, - ) - annotated_logger_mock.assert_logged( - "INFO", - "5", - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - "message_part": 2, - "message_parts": 2, - "split": True, - "split_complete": True, - }, - ) - annotated_logger_mock.assert_logged( - "INFO", - "6" * 199, - present={ - "hostname": "my-host", - "annotated": True, - "runtime": "this function is called every time", - }, - absent=["split", "split_complete", "message_parts", "message_part"], - ) diff --git a/test/test_mocks.py b/test/test_mocks.py deleted file mode 100644 index bfba062..0000000 --- a/test/test_mocks.py +++ /dev/null @@ -1,454 +0,0 @@ -import pytest - -import example.calculator -from example.default import DefaultExample - - -def wrong_message_for_add(message, level): - level = level.upper() - output = f"""No matching log record found. There were 4 log messages. -Desired: -Message: '{message}' -Level: '{level}' -Present: '{{}}' -Absent: 'set()' - -Below is a list of the values for the selected extras for those failed matches. -""" - if level == "DEBUG": - return ( - output - + f""" -[\"Desired message: '{message}', actual message: 'start'\"] -[\"Desired message: '{message}', actual message: 'success'\"] -['Desired level: DEBUG, actual level: INFO', \"Desired message: '{message}', actual message: 'This message will have 'other' as well as 'first' from the annotation above.'\"] -['Desired level: DEBUG, actual level: INFO', \"Desired message: '{message}', actual message: 'This message will have the 'first' annotation and the defaults, but not the 'other''\"] -""".strip() - ) - - if level == "INFO": - return ( - output - + f""" -[\"Desired message: '{message}', actual message: 'This message will have 'other' as well as 'first' from the annotation above.'\"] -[\"Desired message: '{message}', actual message: 'This message will have the 'first' annotation and the defaults, but not the 'other''\"] -['Desired level: INFO, actual level: DEBUG', \"Desired message: '{message}', actual message: 'start'\"] -['Desired level: INFO, actual level: DEBUG', \"Desired message: '{message}', actual message: 'success'\"] -""".strip() - ) - return output - - -def wrong_present_for_add_success(expected, missing=None, incorrect=None): - output = f"""No matching log record found. There were 4 log messages. -Desired: -Message: 'success' -Level: 'DEBUG' -Present: '{expected}' -Absent: 'set()' - -Below is a list of the values for the selected extras for those failed matches. -""" - if missing is None: - missing_string = "" - else: - missing_string = ", ".join([f"'Missing key: `{item}`'" for item in missing]) - if incorrect is None: - incorrect_string = "" - else: - incorrect_string = ", ".join( - [ - f"\"Extra `{d['key']}` value is incorrect. Desired `{d['expected']}` ({type(d['expected'])}) , actual `{d['actual']}` ({type(d['actual'])})\"" - for d in incorrect - ] - ) - if missing_string == "": - ending = incorrect_string - elif incorrect_string == "": - ending = missing_string - else: - ending = missing_string + ", " + incorrect_string - - start_errors = ", ".join( - sorted([f"'Missing key: `{item}`'" for item, _ in expected.items()]) - ) - return ( - output - + f""" -[{ending}] -[\"Desired message: 'success', actual message: 'start'\", {start_errors}] -['Desired level: DEBUG, actual level: INFO', \"Desired message: 'success', actual message: 'This message will have 'other' as well as 'first' from the annotation above.'\", {ending}] -['Desired level: DEBUG, actual level: INFO', \"Desired message: 'success', actual message: 'This message will have the 'first' annotation and the defaults, but not the 'other''\", {ending}] -""".strip() - ) - - -@pytest.mark.usefixtures("_reload_calculator") -class TestAnnotatedLogMock: - def test_assert_logged_message_pass(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - annotated_logger_mock.assert_logged("debug", "start") - annotated_logger_mock.assert_logged( - "info", - "This message will have 'other' as well as 'first' from the annotation above.", - ) - annotated_logger_mock.assert_logged( - "info", - "This message will have the 'first' annotation and the defaults, but not the 'other'", - ) - annotated_logger_mock.assert_logged("debug", "success") - - def test_assert_logged_present_pass(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - annotated_logger_mock.assert_logged("debug", "start", {"extra": "new data"}) - annotated_logger_mock.assert_logged( - "info", - "This message will have 'other' as well as 'first' from the annotation above.", - {"first": 1, "second": 2, "foo": "bar", "other": "value"}, - ) - annotated_logger_mock.assert_logged( - "info", - "This message will have the 'first' annotation and the defaults, but not the 'other'", - {"first": 1, "second": 2, "foo": "bar"}, - ) - annotated_logger_mock.assert_logged( - "debug", "success", {"first": 1, "second": 2, "foo": "bar"} - ) - - def test_assert_logged_absent_pass(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - annotated_logger_mock.assert_logged( - "debug", "start", absent=["first", "second", "other", "foo", "unused"] - ) - annotated_logger_mock.assert_logged( - "info", - "This message will have 'other' as well as 'first' from the annotation above.", - absent=["unused"], - ) - annotated_logger_mock.assert_logged( - "info", - "This message will have the 'first' annotation and the defaults, but not the 'other'", - absent=["unused", "other"], - ) - annotated_logger_mock.assert_logged( - "debug", "success", absent=["unused", "other"] - ) - - def test_assert_logged_present_and_absent_pass(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - annotated_logger_mock.assert_logged( - "debug", - "start", - absent=["first", "second", "other", "foo", "unused"], - present={"extra": "new data"}, - ) - annotated_logger_mock.assert_logged( - "info", - "This message will have 'other' as well as 'first' from the annotation above.", - absent=["unused"], - present={"first": 1, "second": 2, "foo": "bar", "other": "value"}, - ) - annotated_logger_mock.assert_logged( - "info", - "This message will have the 'first' annotation and the defaults, but not the 'other'", - absent=["unused", "other"], - present={"first": 1, "second": 2, "foo": "bar"}, - ) - annotated_logger_mock.assert_logged( - "debug", - "success", - absent=["unused", "other"], - present={"first": 1, "second": 2, "foo": "bar"}, - ) - - def test_assert_logged_no_logs(self, annotated_logger_mock, fail_mock): - annotated_logger_mock.assert_logged("info", "can I haz log pls?") - assert ( - fail_mock.mock_calls[0].args[0] - == """ -No matching log record found. There were 0 log messages. -Desired: -Message: 'can I haz log pls?' -Level: 'INFO' -Present: '{}' -Absent: 'set()' -""".lstrip() - ) - - def test_assert_logged_message_wrong(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - annotated_logger_mock.assert_logged("debug", "wrong: start") - annotated_logger_mock.assert_logged( - "info", - "wrong: This message will have 'other' as well as 'first' from the annotation above.", - ) - annotated_logger_mock.assert_logged( - "info", - "wrong: This message will have the 'first' annotation and the defaults, but not the 'other'", - ) - annotated_logger_mock.assert_logged("debug", "wrong: success") - errors = [ - wrong_message_for_add("wrong: start", "debug"), - wrong_message_for_add( - "wrong: This message will have 'other' as well as 'first' from the annotation above.", - "info", - ), - wrong_message_for_add( - "wrong: This message will have the 'first' annotation and the defaults, but not the 'other'", - "info", - ), - wrong_message_for_add("wrong: success", "debug"), - ] - for i in range(3): - assert fail_mock.mock_calls[i].args[0] == errors[i] - - def test_assert_logged_present_wrong(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - present_values = [ - {"wrong": "key", "also wrong": "missing"}, - {"first": "wrong", "second": 2, "missing": "yes"}, - {"first": "wrong", "second": None}, - ] - for present in present_values: - annotated_logger_mock.assert_logged("debug", "success", present=present) - errors = [ - wrong_present_for_add_success( - expected=present_values[0], missing=["also wrong", "wrong"] - ), - wrong_present_for_add_success( - expected=present_values[1], - missing=["missing"], - incorrect=[{"key": "first", "expected": "wrong", "actual": 1}], - ), - wrong_present_for_add_success( - expected=present_values[2], - incorrect=[ - {"key": "first", "expected": "wrong", "actual": 1}, - {"key": "second", "expected": None, "actual": 2}, - ], - ), - ] - for i in range(len(errors)): - assert fail_mock.mock_calls[i].args[0] == errors[i] - - def test_all_absent_success(self, annotated_logger_mock): - default = DefaultExample() - default.foo() - annotated_logger_mock.assert_logged( - "info", - "foo", - absent=annotated_logger_mock.ALL, - count=1, - ) - - def test_all_absent_fail(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - - message = "This message will have 'other' as well as 'first' from the annotation above." - annotated_logger_mock.assert_logged( - "info", - message, - absent=annotated_logger_mock.ALL, - ) - - assert ( - fail_mock.mock_calls[0].args[0] - == f""" -No matching log record found. There were 4 log messages. -Desired: -Message: '{message}' -Level: '{"INFO"}' -Present: '{{}}' -Absent: 'ALL' - -Below is a list of the values for the selected extras for those failed matches. -['Unwanted key: `extra`', 'Unwanted key: `first`', 'Unwanted key: `foo`', 'Unwanted key: `nested_extra`', 'Unwanted key: `other`', 'Unwanted key: `runtime`', 'Unwanted key: `second`'] -['Desired level: INFO, actual level: DEBUG', "Desired message: 'This message will have 'other' as well as 'first' from the annotation above.', actual message: 'start'", 'Unwanted key: `extra`', 'Unwanted key: `nested_extra`', 'Unwanted key: `runtime`'] -["Desired message: 'This message will have 'other' as well as 'first' from the annotation above.', actual message: 'This message will have the 'first' annotation and the defaults, but not the 'other''", 'Unwanted key: `extra`', 'Unwanted key: `first`', 'Unwanted key: `foo`', 'Unwanted key: `nested_extra`', 'Unwanted key: `runtime`', 'Unwanted key: `second`'] -['Desired level: INFO, actual level: DEBUG', "Desired message: 'This message will have 'other' as well as 'first' from the annotation above.', actual message: 'success'", 'Unwanted key: `extra`', 'Unwanted key: `first`', 'Unwanted key: `foo`', 'Unwanted key: `nested_extra`', 'Unwanted key: `run_time`', 'Unwanted key: `runtime`', 'Unwanted key: `second`', 'Unwanted key: `success`'] -""".strip() - ) - - def test_absent_fail(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.add() - - message = "This message will have 'other' as well as 'first' from the annotation above." - annotated_logger_mock.assert_logged( - "info", - message, - absent=["foo"], - ) - - assert ( - fail_mock.mock_calls[0].args[0] - == f""" -No matching log record found. There were 4 log messages. -Desired: -Message: '{message}' -Level: '{"INFO"}' -Present: '{{}}' -Absent: '{{'foo'}}' - -Below is a list of the values for the selected extras for those failed matches. -['Unwanted key: `foo`'] -['Desired level: INFO, actual level: DEBUG', "Desired message: 'This message will have 'other' as well as 'first' from the annotation above.', actual message: 'start'"] -["Desired message: 'This message will have 'other' as well as 'first' from the annotation above.', actual message: 'This message will have the 'first' annotation and the defaults, but not the 'other''", 'Unwanted key: `foo`'] -['Desired level: INFO, actual level: DEBUG', "Desired message: 'This message will have 'other' as well as 'first' from the annotation above.', actual message: 'success'", 'Unwanted key: `foo`'] -""".strip() - ) - - def test_count_correct(self, annotated_logger_mock): - calc = example.calculator.Calculator(1, 2) - calc.factorial(5) - - annotated_logger_mock.assert_logged( - "info", - "next", - count=5, - ) - - def test_count_wrong(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.factorial(5) - - annotated_logger_mock.assert_logged( - "info", - "next", - count=4, - ) - - assert ( - fail_mock.mock_calls[0].args[0] - == "Found 5 matching messages, 4 were desired" - ) - - def test_count_wrong_message(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.factorial(5) - - annotated_logger_mock.assert_logged( - "info", - "wrong", - count=5, - ) - - assert ( - fail_mock.mock_calls[0].args[0] - == """ -No matching log record found. There were 9 log messages. -Desired: -Count: 5 -Message: 'wrong' -Level: 'INFO' -Present: '{}' -Absent: 'set()' - -Below is a list of the values for the selected extras for those failed matches. -["Desired message: 'wrong', actual message: 'next'"] -["Desired message: 'wrong', actual message: 'success'", "Desired 5 calls, actual 1 call"] -["Desired message: 'wrong', actual message: 'Starting iteration'", "Desired 5 calls, actual 1 call"] -["Desired message: 'wrong', actual message: 'Execution complete'", "Desired 5 calls, actual 1 call"] -['Desired level: INFO, actual level: DEBUG', "Desired message: 'wrong', actual message: 'start'", "Desired 5 calls, actual 1 call"] -""".strip() - ) - - def test_count_wrong_message_and_count(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.factorial(5) - - annotated_logger_mock.assert_logged( - "info", - "wrong", - count=1, - ) - - assert ( - fail_mock.mock_calls[0].args[0] - == """ -No matching log record found. There were 9 log messages. -Desired: -Count: 1 -Message: 'wrong' -Level: 'INFO' -Present: '{}' -Absent: 'set()' - -Below is a list of the values for the selected extras for those failed matches. -["Desired message: 'wrong', actual message: 'success'"] -["Desired message: 'wrong', actual message: 'Starting iteration'"] -["Desired message: 'wrong', actual message: 'Execution complete'"] -['Desired level: INFO, actual level: DEBUG', "Desired message: 'wrong', actual message: 'start'"] -["Desired message: 'wrong', actual message: 'next'", "Desired 1 call, actual 5 calls"] -""".strip() - ) - - def test_count_wrong_present(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(1, 2) - calc.factorial(5) - - annotated_logger_mock.assert_logged( - "info", "next", count=2, present={"value": 9, "temp": True} - ) - - assert ( - fail_mock.mock_calls[0].args[0] - == """ -No matching log record found. There were 9 log messages. -Desired: -Count: 2 -Message: 'next' -Level: 'INFO' -Present: '{'value': 9, 'temp': True}' -Absent: 'set()' - -Below is a list of the values for the selected extras for those failed matches. -["Extra `value` value is incorrect. Desired `9` () , actual `1` ()", "Desired 2 calls, actual 1 call"] -["Extra `value` value is incorrect. Desired `9` () , actual `2` ()", "Desired 2 calls, actual 1 call"] -["Extra `value` value is incorrect. Desired `9` () , actual `3` ()", "Desired 2 calls, actual 1 call"] -["Extra `value` value is incorrect. Desired `9` () , actual `4` ()", "Desired 2 calls, actual 1 call"] -["Extra `value` value is incorrect. Desired `9` () , actual `5` ()", "Desired 2 calls, actual 1 call"] -["Desired message: 'next', actual message: 'success'", 'Missing key: `value`', "Desired 2 calls, actual 1 call"] -["Desired message: 'next', actual message: 'Starting iteration'", 'Missing key: `value`', "Desired 2 calls, actual 1 call"] -["Desired message: 'next', actual message: 'Execution complete'", "Extra `value` value is incorrect. Desired `9` () , actual `5` ()", "Desired 2 calls, actual 1 call"] -['Desired level: INFO, actual level: DEBUG', "Desired message: 'next', actual message: 'start'", 'Missing key: `temp`', 'Missing key: `value`', "Desired 2 calls, actual 1 call"] -""".strip() - ) - - def test_count_zero_correct(self, annotated_logger_mock): - annotated_logger_mock.assert_logged("info", "nope", count=0) - calc = example.calculator.Calculator(7, 2) - calc.factorial(5) - annotated_logger_mock.assert_logged("info", "nope", count=0) - - def test_count_zero_wrong(self, annotated_logger_mock, fail_mock): - calc = example.calculator.Calculator(7, 2) - calc.factorial(5) - annotated_logger_mock.assert_logged("info", "success", count=0) - assert ( - fail_mock.mock_calls[0].args[0] - == "Found 1 matching messages, 0 were desired" - ) - - def test_count_zero_any_message(self, annotated_logger_mock): - annotated_logger_mock.assert_logged("info", count=0) - calc = example.calculator.Calculator(7, 2) - calc.factorial(5) - annotated_logger_mock.assert_logged("warning", count=0) - - def test_count_zero_any_level(self, annotated_logger_mock, fail_mock): - annotated_logger_mock.assert_logged(count=0) - calc = example.calculator.Calculator(7, 2) - calc.factorial(5) - annotated_logger_mock.assert_logged(count=0) - assert ( - fail_mock.mock_calls[0].args[0] - == "Found 9 matching messages, 0 were desired" - ) diff --git a/test/test_plugins.py b/test/test_plugins.py deleted file mode 100644 index 1f144e8..0000000 --- a/test/test_plugins.py +++ /dev/null @@ -1,259 +0,0 @@ -import logging - -import pytest -from requests.exceptions import HTTPError - -from annotated_logger import AnnotatedAdapter, AnnotatedLogger -from annotated_logger.plugins import BasePlugin, RenamerPlugin -from example.actions import ActionsExample -from example.api import ApiClient -from example.calculator import Calculator - - -class SpyPlugin(BasePlugin): - class BoomError(Exception): - pass - - def __init__(self, *, working=True, filter_message=False): - self.exception_triggered = False - self.filter_called = False - self.working = working - self.filter_message = filter_message - - def uncaught_exception( - self, - exception: Exception, # noqa: ARG002 - logger: AnnotatedAdapter, - ) -> AnnotatedAdapter: - if self.working: - self.exception_triggered = True - return logger - raise SpyPlugin.BoomError - - def filter(self, _record: logging.LogRecord) -> bool: - if self.working: - self.filter_called = True - return not self.filter_message - raise SpyPlugin.BoomError - - -@pytest.fixture -def annotated_logger(plugins): - return AnnotatedLogger( - plugins=plugins, - name="annotated_logger.test_plugins", - # I believe that allowing this to set config leads to duplicate - # filters/handlers that re-written on each dictConfig call, - # but aren't overwritten, just hidden/dereferences, but still called - config=False, - ) - - -@pytest.fixture -def plugins(): - return [] - - -@pytest.fixture -def annotate_logs(annotated_logger): - return annotated_logger.annotate_logs - - -@pytest.fixture -def broken_plugin(): - return SpyPlugin(working=False) - - -@pytest.fixture -def working_plugin(): - return SpyPlugin(working=True) - - -@pytest.fixture -def skip_plugin(): - return SpyPlugin(filter_message=True) - - -# Request the annotated_logger fixture so that the config -# has been setup before we get the logger -@pytest.fixture -def annotated_logger_object(annotated_logger): # noqa: ARG001 - return logging.getLogger("annotated_logger") - - -class TestPlugins: - class TestSkip: - @pytest.fixture - def plugins(self, skip_plugin): - return [skip_plugin] - - def test_skip_filter(self, annotated_logger_mock, annotate_logs, skip_plugin): - @annotate_logs() - def should_work(): - return True - - assert isinstance(skip_plugin, SpyPlugin) - assert skip_plugin.filter_called is False - should_work() - assert skip_plugin.filter_called is True - assert annotated_logger_mock.records == [] - - class TestWorking: - @pytest.fixture - def plugins(self, working_plugin): - return [working_plugin] - - def test_working_filter( - self, annotated_logger_mock, annotate_logs, working_plugin - ): - @annotate_logs() - def should_work(): - return True - - assert isinstance(working_plugin, SpyPlugin) - assert working_plugin.filter_called is False - should_work() - assert working_plugin.filter_called is True - annotated_logger_mock.assert_logged("info", "success") - - def test_working_exception(self, annotate_logs, working_plugin): - @annotate_logs() - def throws_exception(): - return 2 / 0 - - assert isinstance(working_plugin, SpyPlugin) - assert working_plugin.exception_triggered is False - with pytest.raises(ZeroDivisionError): - throws_exception() - assert working_plugin.exception_triggered is True - - class TestBroken: - @pytest.fixture - def plugins(self, broken_plugin, working_plugin): - return [broken_plugin, working_plugin, broken_plugin] - - def test_broken_filter( - self, annotated_logger_mock, annotate_logs, broken_plugin - ): - @annotate_logs() - def should_work(): - return True - - assert isinstance(broken_plugin, SpyPlugin) - assert broken_plugin.filter_called is False - should_work() - assert broken_plugin.filter_called is False - assert len(annotated_logger_mock.records) > 0 - assert annotated_logger_mock.records[0].failed_plugins == [ - "", - "", - ] - - def test_broken_exception(self, annotate_logs, broken_plugin): - @annotate_logs() - def throws_exception(): - return 2 / 0 - - assert isinstance(broken_plugin, SpyPlugin) - assert broken_plugin.exception_triggered is False - with pytest.raises(SpyPlugin.BoomError): - throws_exception() - assert broken_plugin.exception_triggered is False - - -class TestRequestsPlugin: - # Test http exception with Calculator.throw_http_exception - def test_logged_http_exception(self, annotated_logger_mock): - client = ApiClient() - with pytest.raises(HTTPError): - client.throw_http_exception() - - annotated_logger_mock.assert_logged( - "ERROR", - "Uncaught Exception in logged function", - { - "action": "example.api:ApiClient.throw_http_exception", - "extra": "new data", - "runtime": "this function is called every time", - "status_code": 418, - "exception_title": "i_am_a_teapot", - "success": False, - }, - ) - - -class TestRenamerPlugin: - def test_joke_should_be_cheezy(self, annotated_logger_mock): - calc = Calculator(1, 9) - calc.divide() - annotated_logger_mock.assert_logged( - "WARNING", - "If you divide by zero you'll create a singularity in the fabric of space-time!", - present={"cheezy_joke": True}, - absent=["joke"], - ) - - def test_field_missing_strict(self, annotated_logger_mock): - annotate_logs = AnnotatedLogger( - plugins=[RenamerPlugin(strict=True, made_up_field="some_other_field")], - name="annotated_logger.test_plugins", - config=False, - ).annotate_logs - - wrapped = annotate_logs(_typing_self=False)(lambda: True) - wrapped() - - assert annotated_logger_mock.records[0].failed_plugins == [ - "" - ] - - def test_field_missing_not_strict(self, annotated_logger_mock): - annotate_logs = AnnotatedLogger( - plugins=[RenamerPlugin(made_up_field="some_other_field")], - name="annotated_logger.test_plugins", - config=False, - ).annotate_logs - - wrapped = annotate_logs(_typing_self=False)(lambda: True) - wrapped() - - record = annotated_logger_mock.records[0] - with pytest.raises( - AttributeError, match="'LogRecord' object has no attribute 'failed_plugins'" - ): - _ = record.failed_plugins - - -class TestNestedRemoverPlugin: - def test_exclude_nested_fields(self, annotated_logger_mock): - calc = Calculator(0, 0) - calc.multiply(2, 21) - annotated_logger_mock.assert_logged( - "INFO", - "Prediction result", - present={"nested_extra": {"nested_key": {}}}, - count=1, - ) - - -@pytest.mark.usefixtures("_reload_actions") -class TestGitHubActionsPlugin: - def test_logs_normally(self, annotated_logger_mock): - action = ActionsExample() - action.first_step() - - annotated_logger_mock.assert_logged("info", "Step 1 running!") - - @pytest.mark.parametrize( - "annotated_logger_object", [logging.getLogger("annotated_logger.actions")] - ) - def test_logs_actions_annotations(self, annotated_logger_mock): - action = ActionsExample() - action.first_step() - action.second_step() - - assert ( - "notice:: Step 1 running! - {'action': 'example.actions:ActionsExample.first_step'}" - in annotated_logger_mock.messages[0] - ) - annotated_logger_mock.assert_logged("DEBUG", count=0)