├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql.yml │ └── pythonpackage.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── _config.yml ├── classifiers.txt ├── doc ├── Makefile ├── make.bat └── source │ ├── LogOnAccess.rst │ ├── PrettyFormat.rst │ ├── _static │ └── .gitkeep │ ├── conf.py │ ├── index.rst │ └── logwrap.rst ├── flake8_requirements.txt ├── logwrap ├── __init__.py ├── constants.py ├── log_on_access.py ├── log_wrap.py ├── py.typed └── repr_utils.py ├── mypy_requirements.txt ├── pyproject.toml ├── pytest_requirements.txt ├── requirements.txt ├── test ├── pyproject.toml ├── test_log_on_access.py ├── test_log_on_access_mod_log.py ├── test_log_wrap.py ├── test_log_wrap_logger.py ├── test_log_wrap_py3.py ├── test_log_wrap_py35.py ├── test_log_wrap_shared.py ├── test_pretty_str.py ├── test_repr_utils.py └── test_repr_utils_special.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,pyx,pxd,pyi}] 12 | indent_size = 4 13 | max_line_length = 120 14 | 15 | [*.rst] 16 | max_line_length = 150 17 | 18 | [Makefile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What do these changes do? 4 | 5 | 6 | 7 | ## Are there changes in behavior for the user? 8 | 9 | 10 | 11 | ## Related issue number 12 | 13 | 14 | 15 | ## Checklist 16 | 17 | - [ ] I think the code is well written 18 | - [ ] Unit tests for the changes exist 19 | - [ ] Documentation reflects the changes 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '45 8 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 27 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'python' ] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 38 | # Use only 'java' to analyze code written in Java, Kotlin or both 39 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v4 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v3 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 56 | # queries: security-extended,security-and-quality 57 | 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v3 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 66 | 67 | # If the Autobuild fails above, remove it and uncomment the following three lines. 68 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 69 | 70 | # - run: | 71 | # echo "Run, Build Application using script" 72 | # ./location_of_script_within_repo/buildscript.sh 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v3 76 | with: 77 | category: "/language:${{matrix.language}}" 78 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'dependabot/**' 7 | tags-ignore: 8 | - '*.[0-9][ab][0-9]' 9 | pull_request: {} 10 | 11 | jobs: 12 | PEP8: 13 | name: Check with Ruff 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install --upgrade ruff 25 | - name: Lint with ruff 26 | run: | 27 | ruff check . --output-format github 28 | 29 | PyLint: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Set up Python 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: '3.11' 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install --upgrade "pylint >= 3.0.0" 41 | - name: Install develop 42 | run: | 43 | pip install -e . 44 | - name: Lint with PyLint 45 | run: | 46 | pylint logwrap 47 | 48 | MyPy: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Set up Python 53 | uses: actions/setup-python@v5 54 | with: 55 | python-version: '3.x' 56 | - name: Install dependencies 57 | run: | 58 | python -m pip install --upgrade pip 59 | pip install --upgrade -r mypy_requirements.txt 60 | - name: Install develop 61 | run: | 62 | pip install -e . 63 | - name: Lint with MyPy 64 | run: | 65 | mypy --strict logwrap 66 | 67 | Black: 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | - name: Set up Python 72 | uses: actions/setup-python@v5 73 | with: 74 | python-version: '3.x' 75 | - name: Install dependencies 76 | run: | 77 | python -m pip install --upgrade pip 78 | pip install --upgrade black regex 79 | - name: Check code style with black 80 | run: | 81 | black --check logwrap 82 | 83 | Refurb: 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v4 87 | - name: Set up Python 88 | uses: actions/setup-python@v5 89 | with: 90 | python-version: '3.x' 91 | - name: Install dependencies 92 | run: | 93 | python -m pip install --upgrade pip 94 | pip install --upgrade refurb 95 | - name: Lint with refurb 96 | run: | 97 | refurb --format github logwrap 98 | Test: 99 | needs: [PEP8, PyLint, MyPy, Black, Refurb] # isort is broken 100 | runs-on: ${{ matrix.os }} 101 | strategy: 102 | max-parallel: 6 103 | matrix: 104 | os: [ubuntu-latest, windows-latest] 105 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 106 | 107 | name: "Script based python ${{ matrix.python-version }} on ${{ matrix.os }}" 108 | steps: 109 | - uses: actions/checkout@v4 110 | - name: Set up Python ${{ matrix.python-version }} 111 | uses: actions/setup-python@v5 112 | with: 113 | python-version: ${{ matrix.python-version }} 114 | - name: Install dependencies 115 | run: | 116 | python -m pip install --upgrade pip wheel 117 | pip install --upgrade -r pytest_requirements.txt 118 | - name: Install develop 119 | run: | 120 | pip install -e . 121 | - name: Test with pytest 122 | run: | 123 | py.test --cov-report= --cov=logwrap test 124 | coverage report -m --fail-under 85 125 | coverage xml 126 | - name: Coveralls Parallel 127 | uses: coverallsapp/github-action@v2 128 | with: 129 | flag-name: run-${{ matrix.python-version }}-${{ matrix.os }} 130 | parallel: true 131 | file: coverage.xml 132 | 133 | UploadCoverage: 134 | name: Upload coverage to Coveralls 135 | needs: [ Test ] 136 | if: ${{ always() }} 137 | runs-on: ubuntu-latest 138 | steps: 139 | - name: Coveralls Finished 140 | uses: coverallsapp/github-action@v2 141 | with: 142 | parallel-finished: true 143 | 144 | Build: 145 | needs: [ Test ] 146 | runs-on: ubuntu-latest 147 | steps: 148 | - uses: actions/checkout@v4 149 | with: 150 | fetch-depth: 0 # need for setuptools_scm 151 | - name: Set up Python 152 | uses: actions/setup-python@v5 153 | with: 154 | python-version: '3.x' 155 | - name: Install dependencies 156 | run: | 157 | python -m pip install --upgrade pip 158 | pip install --upgrade twine build 159 | - name: Build package 160 | run: | 161 | python -m build 162 | - uses: actions/upload-artifact@v4 163 | with: 164 | path: dist/* 165 | name: built-sdist 166 | 167 | Metadata: 168 | name: Validate metadata 169 | runs-on: ubuntu-latest 170 | needs: [ Build ] 171 | steps: 172 | - uses: actions/checkout@v4 173 | - name: Set up Python 174 | uses: actions/setup-python@v5 175 | with: 176 | python-version: '3.x' 177 | cache: 'pip' 178 | - name: Install dependencies 179 | run: | 180 | python -m pip install --upgrade pip 181 | pip install --upgrade twine 182 | - uses: actions/download-artifact@v4.3.0 183 | with: 184 | pattern: built-* 185 | merge-multiple: true 186 | path: dist 187 | - name: Validate metadata 188 | run: | 189 | twine check dist/* 190 | 191 | Deploy: 192 | needs: [Build, Metadata] 193 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 194 | runs-on: ubuntu-latest 195 | environment: 196 | name: pypi 197 | url: https://pypi.org/p/logwrap 198 | permissions: 199 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 200 | steps: 201 | - uses: actions/download-artifact@v4.3.0 202 | with: 203 | pattern: built-* 204 | merge-multiple: true 205 | path: dist 206 | - name: Publish package distributions to PyPI 207 | uses: pypa/gh-action-pypi-publish@release/v1 208 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Test results 2 | /*_result*.xml 3 | /report.html 4 | /mypy_report 5 | /assets/* 6 | .cache/* 7 | .pytest_cache/* 8 | 9 | ### Generated version 10 | /logwrap/_version.py 11 | 12 | ### Generated code 13 | /logwrap/*.c 14 | /pip-wheel-metadata 15 | ### TortoiseGit template 16 | # Project-level settings 17 | /.tgitconfig 18 | ### Eclipse template 19 | 20 | .metadata 21 | bin/ 22 | tmp/ 23 | *.tmp 24 | *.bak 25 | *.swp 26 | *~.nib 27 | local.properties 28 | .settings/ 29 | .loadpath 30 | .recommenders 31 | 32 | # External tool builders 33 | .externalToolBuilders/ 34 | 35 | # Locally stored "Eclipse launch configurations" 36 | *.launch 37 | 38 | # PyDev specific (Python IDE for Eclipse) 39 | *.pydevproject 40 | 41 | # CDT-specific (C/C++ Development Tooling) 42 | .cproject 43 | 44 | # CDT- autotools 45 | .autotools 46 | 47 | # Java annotation processor (APT) 48 | .factorypath 49 | 50 | # PDT-specific (PHP Development Tools) 51 | .buildpath 52 | 53 | # sbteclipse plugin 54 | .target 55 | 56 | # Tern plugin 57 | .tern-project 58 | 59 | # TeXlipse plugin 60 | .texlipse 61 | 62 | # STS (Spring Tool Suite) 63 | .springBeans 64 | 65 | # Code Recommenders 66 | .recommenders/ 67 | 68 | # Scala IDE specific (Scala & Java development for Eclipse) 69 | .cache-main 70 | .scala_dependencies 71 | .worksheet 72 | ### VirtualEnv template 73 | # Virtualenv 74 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 75 | .Python 76 | [Bb]in 77 | [Ii]nclude 78 | [Ll]ib 79 | [Ll]ib64 80 | [Ll]ocal 81 | [Ss]cripts 82 | pyvenv.cfg 83 | .venv 84 | pip-selfcheck.json 85 | ### Kate template 86 | # Swap Files # 87 | .*.kate-swp 88 | .swp.* 89 | ### Python template 90 | # Byte-compiled / optimized / DLL files 91 | __pycache__/ 92 | *.py[cod] 93 | *$py.class 94 | 95 | # C extensions 96 | *.so 97 | 98 | # Distribution / packaging 99 | .Python 100 | build/ 101 | develop-eggs/ 102 | dist/ 103 | downloads/ 104 | eggs/ 105 | .eggs/ 106 | lib/ 107 | lib64/ 108 | parts/ 109 | sdist/ 110 | var/ 111 | wheels/ 112 | *.egg-info/ 113 | .installed.cfg 114 | *.egg 115 | MANIFEST 116 | 117 | # PyInstaller 118 | # Usually these files are written by a python script from a template 119 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 120 | *.manifest 121 | *.spec 122 | 123 | # Installer logs 124 | pip-log.txt 125 | pip-delete-this-directory.txt 126 | 127 | # Unit test / coverage reports 128 | htmlcov/ 129 | .tox/ 130 | .coverage 131 | .coverage.* 132 | .cache 133 | nosetests.xml 134 | coverage.xml 135 | *.cover 136 | .hypothesis/ 137 | 138 | # Translations 139 | *.mo 140 | *.pot 141 | 142 | # Django stuff: 143 | *.log 144 | .static_storage/ 145 | .media/ 146 | local_settings.py 147 | 148 | # Flask stuff: 149 | instance/ 150 | .webassets-cache 151 | 152 | # Scrapy stuff: 153 | .scrapy 154 | 155 | # Sphinx documentation 156 | docs/_build/ 157 | 158 | # PyBuilder 159 | target/ 160 | 161 | # Jupyter Notebook 162 | .ipynb_checkpoints 163 | 164 | # pyenv 165 | .python-version 166 | 167 | # celery beat schedule file 168 | celerybeat-schedule 169 | 170 | # SageMath parsed files 171 | *.sage.py 172 | 173 | # Environments 174 | .env 175 | .venv 176 | env/ 177 | venv/ 178 | ENV/ 179 | env.bak/ 180 | venv.bak/ 181 | 182 | # Spyder project settings 183 | .spyderproject 184 | .spyproject 185 | 186 | # Rope project settings 187 | .ropeproject 188 | 189 | # mkdocs documentation 190 | /site 191 | 192 | # mypy 193 | .mypy_cache/ 194 | ### SublimeText template 195 | # Cache files for Sublime Text 196 | *.tmlanguage.cache 197 | *.tmPreferences.cache 198 | *.stTheme.cache 199 | 200 | # Workspace files are user-specific 201 | *.sublime-workspace 202 | 203 | # Project files should be checked into the repository, unless a significant 204 | # proportion of contributors will probably not be using Sublime Text 205 | # *.sublime-project 206 | 207 | # SFTP configuration file 208 | sftp-config.json 209 | 210 | # Package control specific files 211 | Package Control.last-run 212 | Package Control.ca-list 213 | Package Control.ca-bundle 214 | Package Control.system-ca-bundle 215 | Package Control.cache/ 216 | Package Control.ca-certs/ 217 | Package Control.merged-ca-bundle 218 | Package Control.user-ca-bundle 219 | oscrypto-ca-bundle.crt 220 | bh_unicode_properties.cache 221 | 222 | # Sublime-github package stores a github token in this file 223 | # https://packagecontrol.io/packages/sublime-github 224 | GitHub.sublime-settings 225 | ### Archives template 226 | # It's better to unpack these files and commit the raw source because 227 | # git has its own built in compression methods. 228 | *.7z 229 | *.jar 230 | *.rar 231 | *.zip 232 | *.gz 233 | *.tgz 234 | *.bzip 235 | *.bz2 236 | *.xz 237 | *.lzma 238 | *.cab 239 | 240 | # Packing-only formats 241 | *.iso 242 | *.tar 243 | 244 | # Package management formats 245 | *.dmg 246 | *.xpi 247 | *.gem 248 | *.egg 249 | *.deb 250 | *.rpm 251 | *.msi 252 | *.msm 253 | *.msp 254 | ### Windows template 255 | # Windows thumbnail cache files 256 | Thumbs.db 257 | ehthumbs.db 258 | ehthumbs_vista.db 259 | 260 | # Dump file 261 | *.stackdump 262 | 263 | # Folder config file 264 | [Dd]esktop.ini 265 | 266 | # Recycle Bin used on file shares 267 | $RECYCLE.BIN/ 268 | 269 | # Windows Installer files 270 | *.cab 271 | *.msi 272 | *.msm 273 | *.msp 274 | 275 | # Windows shortcuts 276 | *.lnk 277 | ### C template 278 | # Prerequisites 279 | *.d 280 | 281 | # Object files 282 | *.o 283 | *.ko 284 | *.obj 285 | *.elf 286 | 287 | # Linker output 288 | *.ilk 289 | *.map 290 | *.exp 291 | 292 | # Precompiled Headers 293 | *.gch 294 | *.pch 295 | 296 | # Libraries 297 | *.lib 298 | *.a 299 | *.la 300 | *.lo 301 | 302 | # Shared objects (inc. Windows DLLs) 303 | *.dll 304 | *.so 305 | *.so.* 306 | *.dylib 307 | 308 | # Executables 309 | *.exe 310 | *.out 311 | *.app 312 | *.i*86 313 | *.x86_64 314 | *.hex 315 | 316 | # Debug files 317 | *.dSYM/ 318 | *.su 319 | *.idb 320 | *.pdb 321 | 322 | # Kernel Module Compile Results 323 | *.mod* 324 | *.cmd 325 | .tmp_versions/ 326 | modules.order 327 | Module.symvers 328 | Mkfile.old 329 | dkms.conf 330 | ### macOS template 331 | # General 332 | .DS_Store 333 | .AppleDouble 334 | .LSOverride 335 | 336 | # Icon must end with two \r 337 | Icon 338 | 339 | # Thumbnails 340 | ._* 341 | 342 | # Files that might appear in the root of a volume 343 | .DocumentRevisions-V100 344 | .fseventsd 345 | .Spotlight-V100 346 | .TemporaryItems 347 | .Trashes 348 | .VolumeIcon.icns 349 | .com.apple.timemachine.donotpresent 350 | 351 | # Directories potentially created on remote AFP share 352 | .AppleDB 353 | .AppleDesktop 354 | Network Trash Folder 355 | Temporary Items 356 | .apdisk 357 | ### Emacs template 358 | # -*- mode: gitignore; -*- 359 | *~ 360 | \#*\# 361 | /.emacs.desktop 362 | /.emacs.desktop.lock 363 | *.elc 364 | auto-save-list 365 | tramp 366 | .\#* 367 | 368 | # Org-mode 369 | .org-id-locations 370 | *_archive 371 | 372 | # flymake-mode 373 | *_flymake.* 374 | 375 | # eshell files 376 | /eshell/history 377 | /eshell/lastdir 378 | 379 | # elpa packages 380 | /elpa/ 381 | 382 | # reftex files 383 | *.rel 384 | 385 | # AUCTeX auto folder 386 | /auto/ 387 | 388 | # cask packages 389 | .cask/ 390 | dist/ 391 | 392 | # Flycheck 393 | flycheck_*.el 394 | 395 | # server auth directory 396 | /server/ 397 | 398 | # projectiles files 399 | .projectile 400 | 401 | # directory configuration 402 | .dir-locals.el 403 | ### Vim template 404 | # Swap 405 | [._]*.s[a-v][a-z] 406 | [._]*.sw[a-p] 407 | [._]s[a-v][a-z] 408 | [._]sw[a-p] 409 | 410 | # Session 411 | Session.vim 412 | 413 | # Temporary 414 | .netrwhist 415 | *~ 416 | # Auto-generated tag files 417 | tags 418 | ### GPG template 419 | secring.* 420 | 421 | ### Linux template 422 | *~ 423 | 424 | # temporary files which can be created if a process still has a handle open of a deleted file 425 | .fuse_hidden* 426 | 427 | # KDE directory preferences 428 | .directory 429 | 430 | # Linux trash folder which might appear on any partition or disk 431 | .Trash-* 432 | 433 | # .nfs files are created when an open file is removed but is still being accessed 434 | .nfs* 435 | ### VisualStudio template 436 | ## Ignore Visual Studio temporary files, build results, and 437 | ## files generated by popular Visual Studio add-ons. 438 | ## 439 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 440 | 441 | # User-specific files 442 | *.suo 443 | *.user 444 | *.userosscache 445 | *.sln.docstates 446 | 447 | # User-specific files (MonoDevelop/Xamarin Studio) 448 | *.userprefs 449 | 450 | # Build results 451 | [Dd]ebug/ 452 | [Dd]ebugPublic/ 453 | [Rr]elease/ 454 | [Rr]eleases/ 455 | x64/ 456 | x86/ 457 | bld/ 458 | [Bb]in/ 459 | [Oo]bj/ 460 | [Ll]og/ 461 | 462 | # Visual Studio 2015/2017 cache/options directory 463 | .vs/ 464 | # Uncomment if you have tasks that create the project's static files in wwwroot 465 | #wwwroot/ 466 | 467 | # Visual Studio 2017 auto generated files 468 | Generated\ Files/ 469 | 470 | # MSTest test Results 471 | [Tt]est[Rr]esult*/ 472 | [Bb]uild[Ll]og.* 473 | 474 | # NUNIT 475 | *.VisualState.xml 476 | TestResult.xml 477 | 478 | # Build Results of an ATL Project 479 | [Dd]ebugPS/ 480 | [Rr]eleasePS/ 481 | dlldata.c 482 | 483 | # Benchmark Results 484 | BenchmarkDotNet.Artifacts/ 485 | 486 | # .NET Core 487 | project.lock.json 488 | project.fragment.lock.json 489 | artifacts/ 490 | **/Properties/launchSettings.json 491 | 492 | # StyleCop 493 | StyleCopReport.xml 494 | 495 | # Files built by Visual Studio 496 | *_i.c 497 | *_p.c 498 | *_i.h 499 | *.ilk 500 | *.meta 501 | *.obj 502 | *.pch 503 | *.pdb 504 | *.pgc 505 | *.pgd 506 | *.rsp 507 | *.sbr 508 | *.tlb 509 | *.tli 510 | *.tlh 511 | *.tmp 512 | *.tmp_proj 513 | *.log 514 | *.vspscc 515 | *.vssscc 516 | .builds 517 | *.pidb 518 | *.svclog 519 | *.scc 520 | 521 | # Chutzpah Test files 522 | _Chutzpah* 523 | 524 | # Visual C++ cache files 525 | ipch/ 526 | *.aps 527 | *.ncb 528 | *.opendb 529 | *.opensdf 530 | *.sdf 531 | *.cachefile 532 | *.VC.db 533 | *.VC.VC.opendb 534 | 535 | # Visual Studio profiler 536 | *.psess 537 | *.vsp 538 | *.vspx 539 | *.sap 540 | 541 | # Visual Studio Trace Files 542 | *.e2e 543 | 544 | # TFS 2012 Local Workspace 545 | $tf/ 546 | 547 | # Guidance Automation Toolkit 548 | *.gpState 549 | 550 | # ReSharper is a .NET coding add-in 551 | _ReSharper*/ 552 | *.[Rr]e[Ss]harper 553 | *.DotSettings.user 554 | 555 | # JustCode is a .NET coding add-in 556 | .JustCode 557 | 558 | # TeamCity is a build add-in 559 | _TeamCity* 560 | 561 | # DotCover is a Code Coverage Tool 562 | *.dotCover 563 | 564 | # AxoCover is a Code Coverage Tool 565 | .axoCover/* 566 | !.axoCover/settings.json 567 | 568 | # Visual Studio code coverage results 569 | *.coverage 570 | *.coveragexml 571 | 572 | # NCrunch 573 | _NCrunch_* 574 | .*crunch*.local.xml 575 | nCrunchTemp_* 576 | 577 | # MightyMoose 578 | *.mm.* 579 | AutoTest.Net/ 580 | 581 | # Web workbench (sass) 582 | .sass-cache/ 583 | 584 | # Installshield output folder 585 | [Ee]xpress/ 586 | 587 | # DocProject is a documentation generator add-in 588 | DocProject/buildhelp/ 589 | DocProject/Help/*.HxT 590 | DocProject/Help/*.HxC 591 | DocProject/Help/*.hhc 592 | DocProject/Help/*.hhk 593 | DocProject/Help/*.hhp 594 | DocProject/Help/Html2 595 | DocProject/Help/html 596 | 597 | # Click-Once directory 598 | publish/ 599 | 600 | # Publish Web Output 601 | *.[Pp]ublish.xml 602 | *.azurePubxml 603 | # Note: Comment the next line if you want to checkin your web deploy settings, 604 | # but database connection strings (with potential passwords) will be unencrypted 605 | *.pubxml 606 | *.publishproj 607 | 608 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 609 | # checkin your Azure Web App publish settings, but sensitive information contained 610 | # in these scripts will be unencrypted 611 | PublishScripts/ 612 | 613 | # NuGet Packages 614 | *.nupkg 615 | # The packages folder can be ignored because of Package Restore 616 | **/[Pp]ackages/* 617 | # except build/, which is used as an MSBuild target. 618 | !**/[Pp]ackages/build/ 619 | # Uncomment if necessary however generally it will be regenerated when needed 620 | #!**/[Pp]ackages/repositories.config 621 | # NuGet v3's project.json files produces more ignorable files 622 | *.nuget.props 623 | *.nuget.targets 624 | 625 | # Microsoft Azure Build Output 626 | csx/ 627 | *.build.csdef 628 | 629 | # Microsoft Azure Emulator 630 | ecf/ 631 | rcf/ 632 | 633 | # Windows Store app package directories and files 634 | AppPackages/ 635 | BundleArtifacts/ 636 | Package.StoreAssociation.xml 637 | _pkginfo.txt 638 | *.appx 639 | 640 | # Visual Studio cache files 641 | # files ending in .cache can be ignored 642 | *.[Cc]ache 643 | # but keep track of directories ending in .cache 644 | !*.[Cc]ache/ 645 | 646 | # Others 647 | ClientBin/ 648 | ~$* 649 | *~ 650 | *.dbmdl 651 | *.dbproj.schemaview 652 | *.jfm 653 | *.pfx 654 | *.publishsettings 655 | orleans.codegen.cs 656 | 657 | # Including strong name files can present a security risk 658 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 659 | #*.snk 660 | 661 | # Since there are multiple workflows, uncomment next line to ignore bower_components 662 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 663 | #bower_components/ 664 | 665 | # RIA/Silverlight projects 666 | Generated_Code/ 667 | 668 | # Backup & report files from converting an old project file 669 | # to a newer Visual Studio version. Backup files are not needed, 670 | # because we have git ;-) 671 | _UpgradeReport_Files/ 672 | Backup*/ 673 | UpgradeLog*.XML 674 | UpgradeLog*.htm 675 | 676 | # SQL Server files 677 | *.mdf 678 | *.ldf 679 | *.ndf 680 | 681 | # Business Intelligence projects 682 | *.rdl.data 683 | *.bim.layout 684 | *.bim_*.settings 685 | 686 | # Microsoft Fakes 687 | FakesAssemblies/ 688 | 689 | # GhostDoc plugin setting file 690 | *.GhostDoc.xml 691 | 692 | # Node.js Tools for Visual Studio 693 | .ntvs_analysis.dat 694 | node_modules/ 695 | 696 | # TypeScript v1 declaration files 697 | typings/ 698 | 699 | # Visual Studio 6 build log 700 | *.plg 701 | 702 | # Visual Studio 6 workspace options file 703 | *.opt 704 | 705 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 706 | *.vbw 707 | 708 | # Visual Studio LightSwitch build output 709 | **/*.HTMLClient/GeneratedArtifacts 710 | **/*.DesktopClient/GeneratedArtifacts 711 | **/*.DesktopClient/ModelManifest.xml 712 | **/*.Server/GeneratedArtifacts 713 | **/*.Server/ModelManifest.xml 714 | _Pvt_Extensions 715 | 716 | # Paket dependency manager 717 | .paket/paket.exe 718 | paket-files/ 719 | 720 | # FAKE - F# Make 721 | .fake/ 722 | 723 | # JetBrains Rider 724 | .idea/ 725 | *.sln.iml 726 | 727 | # CodeRush 728 | .cr/ 729 | 730 | # Python Tools for Visual Studio (PTVS) 731 | __pycache__/ 732 | *.pyc 733 | 734 | # Cake - Uncomment if you are using it 735 | # tools/** 736 | # !tools/packages.config 737 | 738 | # Tabs Studio 739 | *.tss 740 | 741 | # Telerik's JustMock configuration file 742 | *.jmconfig 743 | 744 | # BizTalk build output 745 | *.btp.cs 746 | *.btm.cs 747 | *.odx.cs 748 | *.xsd.cs 749 | 750 | # OpenCover UI analysis results 751 | OpenCover/ 752 | 753 | # Azure Stream Analytics local run output 754 | ASALocalRun/ 755 | 756 | # MSBuild Binary and Structured Log 757 | *.binlog 758 | 759 | ### JetBrains template 760 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 761 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 762 | 763 | # User-specific stuff: 764 | .idea/**/workspace.xml 765 | .idea/**/tasks.xml 766 | .idea/dictionaries 767 | 768 | # Sensitive or high-churn files: 769 | .idea/**/dataSources/ 770 | .idea/**/dataSources.ids 771 | .idea/**/dataSources.xml 772 | .idea/**/dataSources.local.xml 773 | .idea/**/sqlDataSources.xml 774 | .idea/**/dynamic.xml 775 | .idea/**/uiDesigner.xml 776 | 777 | # Gradle: 778 | .idea/**/gradle.xml 779 | .idea/**/libraries 780 | 781 | # CMake 782 | cmake-build-debug/ 783 | cmake-build-release/ 784 | 785 | # Mongo Explorer plugin: 786 | .idea/**/mongoSettings.xml 787 | 788 | ## File-based project format: 789 | *.iws 790 | 791 | ## Plugin-specific files: 792 | 793 | # IntelliJ 794 | out/ 795 | 796 | # mpeltonen/sbt-idea plugin 797 | .idea_modules/ 798 | 799 | # JIRA plugin 800 | atlassian-ide-plugin.xml 801 | 802 | # Cursive Clojure plugin 803 | .idea/replstate.xml 804 | 805 | # Crashlytics plugin (for Android Studio and IntelliJ) 806 | com_crashlytics_export_strings.xml 807 | crashlytics.properties 808 | crashlytics-build.properties 809 | fabric.properties 810 | ### VisualStudioCode template 811 | .vscode/* 812 | !.vscode/settings.json 813 | !.vscode/tasks.json 814 | !.vscode/launch.json 815 | !.vscode/extensions.json 816 | 817 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - id: mixed-line-ending 9 | 10 | - repo: https://github.com/pycqa/isort 11 | rev: 6.0.1 12 | hooks: 13 | - id: isort 14 | name: isort (python) 15 | - id: isort 16 | name: isort (cython) 17 | types: [cython] 18 | - id: isort 19 | name: isort (pyi) 20 | types: [pyi] 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 25.1.0 24 | hooks: 25 | - id: black 26 | # It is recommended to specify the latest version of Python 27 | # supported by your project here, or alternatively use 28 | # pre-commit's default_language_version, see 29 | # https://pre-commit.com/#top_level-default_language_version 30 | 31 | - repo: https://github.com/astral-sh/ruff-pre-commit 32 | # Ruff version. 33 | rev: v0.11.8 34 | hooks: 35 | - id: ruff 36 | args: [ --fix, --exit-non-zero-on-fix ] 37 | 38 | - repo: https://github.com/pre-commit/pygrep-hooks 39 | rev: v1.10.0 # Use the ref you want to point at 40 | hooks: 41 | - id: python-check-blanket-noqa 42 | - id: python-check-blanket-type-ignore 43 | - id: rst-directive-colons 44 | - id: rst-inline-touching-normal 45 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # Build documentation in the docs/ directory with Sphinx 4 | sphinx: 5 | builder: html 6 | configuration: doc/source/conf.py 7 | 8 | formats: [] 9 | 10 | python: 11 | version: "3.8" 12 | install: 13 | - requirements: requirements.txt 14 | - method: pip 15 | path: . 16 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @penguinolog @kobe25 @dis-xcom 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project maintainers. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016-2021 Alexey Stepanov aka penguinolog 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE requirements.txt 2 | global-include *.pyx *.pxd 3 | global-exclude *.c 4 | exclude Makefile 5 | prune tools 6 | exclude .travis.yml appveyor.yml azure-pipelines.yml .pyup.yml 7 | exclude tox.ini pytest.ini .coveragerc .pylintrc 8 | exclude .gitignore .dockerignore 9 | prune test 10 | prune .github 11 | prune .azure_pipelines 12 | prune docs 13 | exclude CODEOWNERS CODE_OF_CONDUCT.md _config.yml 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | logwrap 2 | ======= 3 | 4 | .. image:: https://github.com/python-useful-helpers/logwrap/workflows/Python%20package/badge.svg 5 | :target: https://github.com/python-useful-helpers/logwrap/actions 6 | .. image:: https://coveralls.io/repos/github/python-useful-helpers/logwrap/badge.svg?branch=master 7 | :target: https://coveralls.io/github/python-useful-helpers/logwrap?branch=master 8 | .. image:: https://readthedocs.org/projects/logwrap/badge/?version=latest 9 | :target: http://logwrap.readthedocs.io/ 10 | :alt: Documentation Status 11 | .. image:: https://img.shields.io/pypi/v/logwrap.svg 12 | :target: https://pypi.python.org/pypi/logwrap 13 | .. image:: https://img.shields.io/pypi/pyversions/logwrap.svg 14 | :target: https://pypi.python.org/pypi/logwrap 15 | .. image:: https://img.shields.io/pypi/status/logwrap.svg 16 | :target: https://pypi.python.org/pypi/logwrap 17 | .. image:: https://img.shields.io/github/license/python-useful-helpers/logwrap.svg 18 | :target: https://raw.githubusercontent.com/python-useful-helpers/logwrap/master/LICENSE 19 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 20 | :target: https://github.com/ambv/black 21 | 22 | 23 | logwrap is a helper for logging in human-readable format function arguments and call result on function call. 24 | Why? Because logging of `*args, **kwargs` become useless with project grow and you need more details in call log. 25 | 26 | Cons: 27 | 28 | * Log records are not single line. 29 | 30 | Pros: 31 | 32 | * Log records are not single 100500 symbols length line. 33 | (Especially actual for testing/development environments and for Kibana users). 34 | * Service free: job is done by this library and it's dependencies. It works at virtualenv 35 | * Free software: Apache license 36 | * Open Source: https://github.com/python-useful-helpers/logwrap 37 | * PyPI packaged: https://pypi.python.org/pypi/logwrap 38 | * Self-documented code: docstrings with types in comments 39 | * Tested: see bages on top 40 | 41 | This package includes helpers: 42 | 43 | * `logwrap` - main helper. The same is `LogWrap`. 44 | 45 | * `LogWrap` - class with `logwrap` implementation. May be used directly. 46 | 47 | * `pretty_repr` 48 | 49 | * `pretty_str` 50 | 51 | * `PrettyFormat` 52 | 53 | * `LogOnAccess` - property with logging on successful get/set/delete or failure. 54 | 55 | Usage 56 | ===== 57 | 58 | logwrap 59 | ------- 60 | The main decorator. Could be used as not argumented (`@logwrap.logwrap`) and argumented (`@logwrap.logwrap()`). 61 | Not argumented usage simple calls with default values for all positions. 62 | 63 | .. note:: Argumens should be set via keywords only. 64 | 65 | Argumented usage with arguments from signature: 66 | 67 | .. code-block:: python 68 | 69 | @logwrap.logwrap( 70 | log=None, # if not set: try to find LOGGER, LOG, logger or log object in target module and use it if it logger instance. Fallback: logger named logwrap 71 | log_level=logging.DEBUG, 72 | exc_level=logging.ERROR, 73 | max_indent=20, # forwarded to the pretty_repr, 74 | max_iter=0, # forwarded to the pretty_repr, max number of items in the Iterable before ellipsis. Unlimited if 0. 75 | blacklisted_names=None, # list argument names, which should be dropped from log 76 | blacklisted_exceptions=None, # Exceptions to skip details in log (no traceback, no exception details - just class name) 77 | log_call_args=True, # Log call arguments before call 78 | log_call_args_on_exc=True, # Log call arguments if exception happens 79 | log_traceback = True, # Log traceback if exception happens 80 | log_result_obj=True, # Log result object 81 | ) 82 | 83 | Usage examples: 84 | 85 | .. code-block:: python 86 | 87 | @logwrap.logwrap() 88 | def foo(): 89 | pass 90 | 91 | is equal to: 92 | 93 | .. code-block:: python 94 | 95 | @logwrap.logwrap 96 | def foo(): 97 | pass 98 | 99 | Get decorator for use without parameters: 100 | 101 | .. code-block:: python 102 | 103 | get_logs = logwrap.logwrap() # set required parameters via arguments 104 | 105 | type(get_logs) == LogWrap # All logic is implemented in LogWrap class starting from version 2.2.0 106 | 107 | @get_logs 108 | def foo(): 109 | pass 110 | 111 | Call example (python 3.8): 112 | 113 | .. code-block:: python 114 | 115 | import logwrap 116 | 117 | @logwrap.logwrap 118 | def example_function1( 119 | arg0: str, 120 | /, 121 | arg1: str, 122 | arg2: str='arg2', 123 | *args, 124 | kwarg1: str, 125 | kwarg2: str='kwarg2', 126 | **kwargs 127 | ) -> tuple(): 128 | return (arg0, arg1, arg2, args, kwarg1, kwarg2, kwargs) 129 | 130 | example_function1('arg0', 'arg1', kwarg1='kwarg1', kwarg3='kwarg3') 131 | 132 | This code during execution will produce log records: 133 | 134 | :: 135 | 136 | Calling: 137 | 'example_function1'( 138 | # POSITIONAL_ONLY: 139 | arg0='arg0', # type: str 140 | # POSITIONAL_OR_KEYWORD: 141 | arg1='arg1', # type: str 142 | arg2='arg2', # type: str 143 | # VAR_POSITIONAL: 144 | args=(), 145 | # KEYWORD_ONLY: 146 | kwarg1='kwarg1', # type: str 147 | kwarg2='kwarg2', # type: str 148 | # VAR_KEYWORD: 149 | kwargs={ 150 | 'kwarg3': 'kwarg3', 151 | }, 152 | ) 153 | Done: 'example_function1' with result: 154 | 155 | ( 156 | 'arg0', 157 | 'arg1', 158 | 'arg2', 159 | (), 160 | 'kwarg1', 161 | 'kwarg2', 162 | { 163 | 'kwarg3': 'kwarg3', 164 | }, 165 | ) 166 | 167 | LogWrap 168 | ------- 169 | Example construction and read from test: 170 | 171 | .. code-block:: python 172 | 173 | log_call = logwrap.LogWrap() 174 | log_call.log_level == logging.DEBUG 175 | log_call.exc_level == logging.ERROR 176 | log_call.max_indent == 20 177 | log_call.blacklisted_names == [] 178 | log_call.blacklisted_exceptions == [] 179 | log_call.log_call_args == True 180 | log_call.log_call_args_on_exc == True 181 | log_call.log_traceback == True 182 | log_call.log_result_obj == True 183 | 184 | On object change, variable types is validated. 185 | 186 | In special cases, when special processing required for parameters logging (hide or change parameters in log), 187 | it can be done by override `pre_process_param` and `post_process_param`. 188 | 189 | See API documentation for details. 190 | 191 | 192 | pretty_repr 193 | ----------- 194 | This is specified helper for making human-readable repr on complex objects. 195 | Signature is self-documenting: 196 | 197 | .. code-block:: python 198 | 199 | def pretty_repr( 200 | src, # object for repr 201 | indent=0, # start indent 202 | no_indent_start=False, # do not indent the first level 203 | max_indent=20, # maximum allowed indent level 204 | indent_step=4, # step between indents 205 | ) 206 | 207 | 208 | pretty_str 209 | ---------- 210 | This is specified helper for making human-readable str on complex objects. 211 | Signature is self-documenting: 212 | 213 | .. code-block:: python 214 | 215 | def pretty_str( 216 | src, # object for __str__ 217 | indent=0, # start indent 218 | no_indent_start=False, # do not indent the first level 219 | max_indent=20, # maximum allowed indent level 220 | indent_step=4, # step between indents 221 | ) 222 | 223 | Limitations: 224 | Dict like objects is always marked inside `{}` for readability, even if it is `collections.OrderedDict` (standard repr as list of tuples). 225 | 226 | Iterable types is not declared, only brackets is used. 227 | 228 | String and bytes looks the same (its __str__, not __repr__). 229 | 230 | PrettyFormat 231 | ------------ 232 | PrettyFormat is the main formatting implementation class. 233 | `pretty_repr` and `pretty_str` uses instances of subclasses `PrettyRepr` and `PrettyStr` from this class. 234 | This class is mostly exposed for typing reasons. 235 | Object signature: 236 | 237 | .. code-block:: python 238 | 239 | def __init__( 240 | self, 241 | max_indent=20, # maximum allowed indent level 242 | indent_step=4, # step between indents 243 | ) 244 | 245 | Callable object (`PrettyFormat` instance) signature: 246 | 247 | .. code-block:: python 248 | 249 | def __call__( 250 | self, 251 | src, # object for repr 252 | indent=0, # start indent 253 | no_indent_start=False # do not indent the first level 254 | ) 255 | 256 | Adopting your code 257 | ------------------ 258 | pretty_repr behavior could be overridden for your classes by implementing specific magic method: 259 | 260 | .. code-block:: python 261 | 262 | def __pretty_repr__( 263 | self, 264 | parser # PrettyFormat class instance, 265 | indent # start indent, 266 | no_indent_start # do not indent the first level 267 | ): 268 | return ... 269 | 270 | This method will be executed instead of __repr__ on your object. 271 | 272 | .. code-block:: python 273 | 274 | def __pretty_str__( 275 | self, 276 | parser # PrettyFormat class instance, 277 | indent # start indent, 278 | no_indent_start # do not indent the first level 279 | ): 280 | return ... 281 | 282 | This method will be executed instead of __str__ on your object. 283 | 284 | LogOnAccess 285 | ----------- 286 | 287 | This special case of property is useful in cases, where a lot of properties should be logged by similar way without writing a lot of code. 288 | 289 | Basic API is conform with `property`, but in addition it is possible to customize logger, log levels and log conditions. 290 | 291 | Usage examples: 292 | 293 | 1. Simple usage. All by default. 294 | logger is re-used: 295 | 296 | * from instance if available with names `logger` or `log`, 297 | * from instance module if available with names `LOGGER`, `log`, 298 | * else used internal `logwrap.log_on_access` logger. 299 | 300 | .. code-block:: python 301 | 302 | import logging 303 | 304 | class Target(object): 305 | 306 | def init(self, val='ok') 307 | self.val = val 308 | self.logger = logging.get_logger(self.__class__.__name__) # Single for class, follow subclassing 309 | 310 | def __repr__(self): 311 | return "{cls}(val={self.val})".format(cls=self.__class__.__name__, self=self) 312 | 313 | @logwrap.LogOnAccess 314 | def ok(self): 315 | return self.val 316 | 317 | @ok.setter 318 | def ok(self, val): 319 | self.val = val 320 | 321 | @ok.deleter 322 | def ok(self): 323 | self.val = "" 324 | 325 | 2. Use with global logger for class: 326 | 327 | .. code-block:: python 328 | 329 | class Target(object): 330 | 331 | def init(self, val='ok') 332 | self.val = val 333 | 334 | def __repr__(self): 335 | return "{cls}(val={self.val})".format(cls=self.__class__.__name__, self=self) 336 | 337 | @logwrap.LogOnAccess 338 | def ok(self): 339 | return self.val 340 | 341 | @ok.setter 342 | def ok(self, val): 343 | self.val = val 344 | 345 | @ok.deleter 346 | def ok(self): 347 | self.val = "" 348 | 349 | ok.logger = 'test_logger' 350 | ok.log_level = logging.INFO 351 | ok.exc_level = logging.ERROR 352 | ok.log_object_repr = True # As by default 353 | ok.log_before = True # As by default 354 | ok.log_success = True # As by default 355 | ok.log_failure = True # As by default 356 | ok.log_traceback = True # As by default 357 | ok.override_name = None # As by default: use original name 358 | 359 | Testing 360 | ======= 361 | The main test mechanism for the package `logwrap` is using `tox`. 362 | Available environments can be collected via `tox -l` 363 | 364 | CI/CD systems 365 | ============= 366 | 367 | `GitHub: `_ is used for functional tests. 368 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /classifiers.txt: -------------------------------------------------------------------------------- 1 | Development Status :: 5 - Production/Stable 2 | Intended Audience :: Developers 3 | Topic :: Software Development :: Libraries :: Python Modules 4 | License :: OSI Approved :: Apache Software License 5 | Programming Language :: Python :: 3 6 | Programming Language :: Python :: 3 :: Only 7 | Programming Language :: Python :: 3.9 8 | Programming Language :: Python :: 3.10 9 | Programming Language :: Python :: 3.11 10 | Programming Language :: Python :: 3.12 11 | Programming Language :: Python :: 3.13 12 | Programming Language :: Python :: Implementation :: CPython 13 | Programming Language :: Python :: Implementation :: PyPy 14 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/logwrap.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/logwrap.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/logwrap" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/logwrap" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\logwrap.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\logwrap.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /doc/source/LogOnAccess.rst: -------------------------------------------------------------------------------- 1 | .. LogOnAccess 2 | 3 | API: LogOnAccess 4 | ======================== 5 | 6 | .. py:module:: logwrap 7 | .. py:currentmodule:: logwrap 8 | 9 | 10 | .. py:class:: LogOnAccess(property) 11 | 12 | Property with logging on successful get/set/delete or failure. 13 | 14 | .. versionadded:: 6.1.0 15 | 16 | .. py:method:: __init__(fget=None, fset=None, fdel=None, doc=None, *, logger=None, log_object_repr=True, log_level=logging.DEBUG, exc_level=logging.DEBUG, log_before=True, log_success=True, log_failure=True, log_traceback=True, override_name=None) 17 | 18 | :param fget: normal getter. 19 | :type fget: None | Callable[[typing.Any, ], typing.Any] 20 | :param fset: normal setter. 21 | :type fset: None | Callable[[typing.Any, typing.Any], None] 22 | :param fdel: normal deleter. 23 | :type fdel: None | Callable[[typing.Any, ], None] 24 | :param doc: docstring override 25 | :type doc: None | str 26 | :param logger: logger instance or name to use as override 27 | :type logger: None | logging.Logger | str 28 | :param log_object_repr: use `repr` over object to describe owner if True else owner class name and id 29 | :type log_object_repr: bool 30 | :param log_level: log level for successful operations 31 | :type log_level: int 32 | :param exc_level: log level for exceptions 33 | :type exc_level: int 34 | :param log_before: log before operation 35 | :type log_before: bool 36 | :param log_success: log successful operations 37 | :type log_success: bool 38 | :param log_failure: log exceptions 39 | :type log_failure: bool 40 | :param log_traceback: Log traceback on exceptions 41 | :type log_traceback: bool 42 | :param override_name: override property name if not None else use getter/setter/deleter name 43 | :type override_name: None | str 44 | 45 | .. py:method:: getter(fget) 46 | 47 | Descriptor to change the getter on a property. 48 | 49 | :param fget: new normal getter. 50 | :type fget: ``None | Callable[[typing.Any, ], typing.Any]`` 51 | :rtype: ``AdvancedProperty`` 52 | 53 | .. py:method:: setter(fset) 54 | 55 | Descriptor to change the setter on a property. 56 | 57 | :param fset: new setter. 58 | :type fset: ``None | Callable[[typing.Any, typing.Any], None]`` 59 | :rtype: ``AdvancedProperty`` 60 | 61 | .. py:method:: deleter(fdel) 62 | 63 | Descriptor to change the deleter on a property. 64 | 65 | :param fdel: New deleter. 66 | :type fdel: ``None | Callable[[typing.Any, ], None]`` 67 | :rtype: ``AdvancedProperty`` 68 | 69 | .. py:attribute:: fget 70 | 71 | ``None | Callable[[typing.Any, ], typing.Any]`` 72 | Getter instance. 73 | 74 | .. py:attribute:: fset 75 | 76 | ``None | Callable[[typing.Any, typing.Any], None]`` 77 | Setter instance. 78 | 79 | .. py:attribute:: fdel 80 | 81 | ``None | Callable[[typing.Any, ], None]`` 82 | Deleter instance. 83 | 84 | .. py:attribute:: logger 85 | 86 | ``None | logging.Logger`` 87 | Logger instance to use as override. 88 | 89 | .. py:attribute:: log_object_repr 90 | 91 | ``bool`` 92 | Use `repr` over object to describe owner if True else owner class name and id. 93 | 94 | .. py:attribute:: log_level 95 | 96 | ``int`` 97 | Log level for successful operations. 98 | 99 | .. py:attribute:: exc_level 100 | 101 | ``int`` 102 | Log level for exceptions. 103 | 104 | .. py:attribute:: log_before 105 | 106 | ``bool`` 107 | Log before operation 108 | 109 | .. py:attribute:: log_success 110 | 111 | ``bool`` 112 | Log successful operations. 113 | 114 | .. py:attribute:: log_failure 115 | 116 | ``bool`` 117 | Log exceptions. 118 | 119 | .. py:attribute:: log_traceback 120 | 121 | ``bool`` 122 | Log traceback on exceptions. 123 | 124 | .. py:attribute:: override_name 125 | 126 | ``None | str`` 127 | Override property name if not None else use getter/setter/deleter name. 128 | -------------------------------------------------------------------------------- /doc/source/PrettyFormat.rst: -------------------------------------------------------------------------------- 1 | .. PrettyFormat, pretty_repr and pretty_str 2 | 3 | API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. 4 | ======================================================================== 5 | 6 | .. py:module:: logwrap 7 | .. py:currentmodule:: logwrap 8 | 9 | .. py:function:: pretty_repr(src, indent=0, no_indent_start=False, max_indent=20, indent_step=4, ) 10 | 11 | Make human readable repr of object. 12 | 13 | :param src: object to process 14 | :type src: typing.Any 15 | :param indent: start indentation, all next levels is +indent_step 16 | :type indent: int 17 | :param no_indent_start: do not indent open bracket and simple parameters 18 | :type no_indent_start: bool 19 | :param max_indent: maximal indent before classic repr() call 20 | :type max_indent: int 21 | :param indent_step: step for the next indentation level 22 | :type indent_step: int 23 | :return: formatted string 24 | :rtype: str 25 | 26 | 27 | .. py:function:: pretty_str(src, indent=0, no_indent_start=False, max_indent=20, indent_step=4, ) 28 | 29 | Make human readable str of object. 30 | 31 | .. versionadded:: 1.1.0 32 | 33 | :param src: object to process 34 | :type src: typing.Any 35 | :param indent: start indentation, all next levels is +indent_step 36 | :type indent: int 37 | :param no_indent_start: do not indent open bracket and simple parameters 38 | :type no_indent_start: bool 39 | :param max_indent: maximal indent before classic repr() call 40 | :type max_indent: int 41 | :param indent_step: step for the next indentation level 42 | :type indent_step: int 43 | :return: formatted string 44 | :rtype: str 45 | 46 | 47 | .. py:class:: PrettyFormat(object) 48 | 49 | Designed for usage as __repr__ and __str__ replacement on complex objects 50 | 51 | .. versionadded:: 1.0.2 52 | .. versionchanged:: 3.0.1 53 | 54 | .. py:method:: __init__(max_indent=20, indent_step=4, ) 55 | 56 | :param max_indent: maximal indent before classic repr() call 57 | :type max_indent: int 58 | :param indent_step: step for the next indentation level 59 | :type indent_step: int 60 | 61 | .. note:: Attributes is read-only 62 | 63 | .. py:attribute:: max_indent 64 | 65 | .. py:attribute:: indent_step 66 | 67 | .. py:method:: next_indent(indent, multiplier=1) 68 | 69 | Next indentation value. Used internally and on __pretty_{keyword}__ calls. 70 | 71 | :param indent: current indentation value 72 | :type indent: int 73 | :param multiplier: step multiplier 74 | :type multiplier: int 75 | :rtype: int 76 | 77 | .. py:method:: process_element(src, indent=0, no_indent_start=False) 78 | 79 | Make human readable representation of object. 80 | 81 | :param src: object to process 82 | :type src: typing.Any 83 | :param indent: start indentation 84 | :type indent: int 85 | :param no_indent_start: 86 | do not indent open bracket and simple parameters 87 | :type no_indent_start: bool 88 | :return: formatted string 89 | :rtype: str 90 | 91 | .. py:method:: __call__(src, indent=0, no_indent_start=False) 92 | 93 | Make human readable representation of object. The main entry point. 94 | 95 | :param src: object to process 96 | :type src: typing.Any 97 | :param indent: start indentation 98 | :type indent: int 99 | :param no_indent_start: 100 | do not indent open bracket and simple parameters 101 | :type no_indent_start: bool 102 | :return: formatted string 103 | :rtype: str 104 | 105 | 106 | .. py:class:: PrettyRepr(PrettyFormat) 107 | 108 | Designed for usage as __repr__ replacement on complex objects 109 | 110 | .. versionadded:: 3.0.0 111 | .. versionchanged:: 3.0.1 112 | 113 | .. py:method:: __init__(max_indent=20, indent_step=4, ) 114 | 115 | :param max_indent: maximal indent before classic repr() call 116 | :type max_indent: int 117 | :param indent_step: step for the next indentation level 118 | :type indent_step: int 119 | 120 | 121 | .. py:class:: PrettyStr(PrettyFormat) 122 | 123 | Designed for usage as __repr__ replacement on complex objects 124 | 125 | .. versionadded:: 3.0.0 126 | .. versionchanged:: 3.0.1 127 | 128 | .. py:method:: __init__(max_indent=20, indent_step=4, ) 129 | 130 | :param max_indent: maximal indent before classic repr() call 131 | :type max_indent: int 132 | :param indent_step: step for the next indentation level 133 | :type indent_step: int 134 | -------------------------------------------------------------------------------- /doc/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-useful-helpers/logwrap/6ceee343c6184f7101078c78e976b0851d9da260/doc/source/_static/.gitkeep -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | """autogenerated""" 2 | 3 | # 4 | # logwrap documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Oct 30 13:40:52 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import pkg_resources 17 | 18 | release = pkg_resources.get_distribution("logwrap").version 19 | version = ".".join(release.split(".")[:2]) 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | # 25 | # import os 26 | # import sys 27 | # sys.path.insert(0, os.path.abspath('.')) 28 | 29 | # -- General configuration ------------------------------------------------ 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | "sphinx.ext.autodoc", 40 | "sphinx.ext.doctest", 41 | "sphinx.ext.coverage", 42 | "sphinx.ext.viewcode", 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ["_templates"] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = ".rst" 53 | 54 | # The encoding of source files. 55 | # 56 | # source_encoding = 'utf-8-sig' 57 | 58 | # The master toctree document. 59 | master_doc = "index" 60 | 61 | # General information about the project. 62 | project = "logwrap" 63 | copyright = "2016-2023, Alexey Stepanov" # noqa: A001 64 | author = "Aleksei Stepanov" 65 | 66 | # The version info for the project you're documenting, acts as replacement for 67 | # |version| and |release|, also used in various other places throughout the 68 | # built documents. 69 | # 70 | # The short X.Y version. 71 | 72 | # The full version, including alpha/beta/rc tags. 73 | 74 | # The language for content autogenerated by Sphinx. Refer to documentation 75 | # for a list of supported languages. 76 | # 77 | # This is also used if you do content translation via gettext catalogs. 78 | # Usually you set "language" from the command line for these cases. 79 | language = "en" 80 | 81 | # There are two options for replacing |today|: either, you set today to some 82 | # non-false value, then it is used: 83 | # 84 | # today = '' 85 | # 86 | # Else, today_fmt is used as the format for a strftime call. 87 | # 88 | # today_fmt = '%B %d, %Y' 89 | 90 | # List of patterns, relative to source directory, that match files and 91 | # directories to ignore when looking for source files. 92 | # This patterns also effect to html_static_path and html_extra_path 93 | exclude_patterns = [] 94 | 95 | # The reST default role (used for this markup: `text`) to use for all 96 | # documents. 97 | # 98 | # default_role = None 99 | 100 | # If true, '()' will be appended to :func: etc. cross-reference text. 101 | # 102 | # add_function_parentheses = True 103 | 104 | # If true, the current module name will be prepended to all description 105 | # unit titles (such as .. function::). 106 | # 107 | # add_module_names = True 108 | 109 | # If true, sectionauthor and moduleauthor directives will be shown in the 110 | # output. They are ignored by default. 111 | # 112 | # show_authors = False 113 | 114 | # The name of the Pygments (syntax highlighting) style to use. 115 | pygments_style = "sphinx" 116 | 117 | # A list of ignored prefixes for module index sorting. 118 | # modindex_common_prefix = [] 119 | 120 | # If true, keep warnings as "system message" paragraphs in the built documents. 121 | # keep_warnings = False 122 | 123 | # If true, `todo` and `todoList` produce output, else they produce nothing. 124 | todo_include_todos = False 125 | 126 | 127 | # -- Options for HTML output ---------------------------------------------- 128 | 129 | # The theme to use for HTML and HTML Help pages. See the documentation for 130 | # a list of builtin themes. 131 | # 132 | html_theme = "alabaster" 133 | 134 | # Theme options are theme-specific and customize the look and feel of a theme 135 | # further. For a list of options available for each theme, see the 136 | # documentation. 137 | # 138 | html_theme_options = { 139 | "page_width": "auto", 140 | } 141 | 142 | # Add any paths that contain custom themes here, relative to this directory. 143 | # html_theme_path = [] 144 | 145 | # The name for this set of Sphinx documents. 146 | # " v documentation" by default. 147 | # 148 | # html_title = 'logwrap v0.5' 149 | 150 | # A shorter title for the navigation bar. Default is the same as html_title. 151 | # 152 | # html_short_title = None 153 | 154 | # The name of an image file (relative to this directory) to place at the top 155 | # of the sidebar. 156 | # 157 | # html_logo = None 158 | 159 | # The name of an image file (relative to this directory) to use as a favicon of 160 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 161 | # pixels large. 162 | # 163 | # html_favicon = None 164 | 165 | # Add any paths that contain custom static files (such as style sheets) here, 166 | # relative to this directory. They are copied after the builtin static files, 167 | # so a file named "default.css" will overwrite the builtin "default.css". 168 | html_static_path = ["_static"] 169 | 170 | # Add any extra paths that contain custom files (such as robots.txt or 171 | # .htaccess) here, relative to this directory. These files are copied 172 | # directly to the root of the documentation. 173 | # 174 | # html_extra_path = [] 175 | 176 | # If not None, a 'Last updated on:' timestamp is inserted at every page 177 | # bottom, using the given strftime format. 178 | # The empty string is equivalent to '%b %d, %Y'. 179 | # 180 | # html_last_updated_fmt = None 181 | 182 | # If true, SmartyPants will be used to convert quotes and dashes to 183 | # typographically correct entities. 184 | # 185 | # html_use_smartypants = True 186 | 187 | # Custom sidebar templates, maps document names to template names. 188 | # 189 | # html_sidebars = {} 190 | 191 | # Additional templates that should be rendered to pages, maps page names to 192 | # template names. 193 | # 194 | # html_additional_pages = {} 195 | 196 | # If false, no module index is generated. 197 | # 198 | # html_domain_indices = True 199 | 200 | # If false, no index is generated. 201 | # 202 | # html_use_index = True 203 | 204 | # If true, the index is split into individual pages for each letter. 205 | # 206 | # html_split_index = False 207 | 208 | # If true, links to the reST sources are added to the pages. 209 | # 210 | # html_show_sourcelink = True 211 | 212 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 213 | # 214 | # html_show_sphinx = True 215 | 216 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 217 | # 218 | # html_show_copyright = True 219 | 220 | # If true, an OpenSearch description file will be output, and all pages will 221 | # contain a tag referring to it. The value of this option must be the 222 | # base URL from which the finished HTML is served. 223 | # 224 | # html_use_opensearch = '' 225 | 226 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 227 | # html_file_suffix = None 228 | 229 | # Language to be used for generating the HTML full-text search index. 230 | # Sphinx supports the following languages: 231 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 232 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 233 | # 234 | # html_search_language = 'en' 235 | 236 | # A dictionary with options for the search language support, empty by default. 237 | # 'ja' uses this config value. 238 | # 'zh' user can custom change `jieba` dictionary path. 239 | # 240 | # html_search_options = {'type': 'default'} 241 | 242 | # The name of a javascript file (relative to the configuration directory) that 243 | # implements a search results scorer. If empty, the default will be used. 244 | # 245 | # html_search_scorer = 'scorer.js' 246 | 247 | # Output file base name for HTML help builder. 248 | htmlhelp_basename = "logwrapdoc" 249 | 250 | # -- Options for LaTeX output --------------------------------------------- 251 | 252 | latex_elements = { 253 | # The paper size ('letterpaper' or 'a4paper'). 254 | # 255 | # 'papersize': 'letterpaper', 256 | # The font size ('10pt', '11pt' or '12pt'). 257 | # 258 | # 'pointsize': '10pt', 259 | # Additional stuff for the LaTeX preamble. 260 | # 261 | # 'preamble': '', 262 | # Latex figure (float) alignment 263 | # 264 | # 'figure_align': 'htbp', 265 | } 266 | 267 | # Grouping the document tree into LaTeX files. List of tuples 268 | # (source start file, target name, title, 269 | # author, documentclass [howto, manual, or own class]). 270 | latex_documents = [ 271 | (master_doc, "logwrap.tex", "logwrap Documentation", "Alexey Stepanov", "manual"), 272 | ] 273 | 274 | # The name of an image file (relative to this directory) to place at the top of 275 | # the title page. 276 | # 277 | # latex_logo = None 278 | 279 | # For "manual" documents, if this is true, then toplevel headings are parts, 280 | # not chapters. 281 | # 282 | # latex_use_parts = False 283 | 284 | # If true, show page references after internal links. 285 | # 286 | # latex_show_pagerefs = False 287 | 288 | # If true, show URL addresses after external links. 289 | # 290 | # latex_show_urls = False 291 | 292 | # Documents to append as an appendix to all manuals. 293 | # 294 | # latex_appendices = [] 295 | 296 | # It false, will not define \strong, \code, itleref, \crossref ... but only 297 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 298 | # packages. 299 | # 300 | # latex_keep_old_macro_names = True 301 | 302 | # If false, no module index is generated. 303 | # 304 | # latex_domain_indices = True 305 | 306 | 307 | # -- Options for manual page output --------------------------------------- 308 | 309 | # One entry per manual page. List of tuples 310 | # (source start file, name, description, authors, manual section). 311 | man_pages = [(master_doc, "logwrap", "logwrap Documentation", [author], 1)] 312 | 313 | # If true, show URL addresses after external links. 314 | # 315 | # man_show_urls = False 316 | 317 | 318 | # -- Options for Texinfo output ------------------------------------------- 319 | 320 | # Grouping the document tree into Texinfo files. List of tuples 321 | # (source start file, target name, title, author, 322 | # dir menu entry, description, category) 323 | texinfo_documents = [ 324 | ( 325 | master_doc, 326 | "logwrap", 327 | "logwrap Documentation", 328 | author, 329 | "logwrap", 330 | "One line description of project.", 331 | "Miscellaneous", 332 | ), 333 | ] 334 | 335 | # Documents to append as an appendix to all manuals. 336 | # 337 | # texinfo_appendices = [] 338 | 339 | # If false, no module index is generated. 340 | # 341 | # texinfo_domain_indices = True 342 | 343 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 344 | # 345 | # texinfo_show_urls = 'footnote' 346 | 347 | # If true, do not generate a @detailmenu in the "Top" node's menu. 348 | # 349 | # texinfo_no_detailmenu = False 350 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. logwrap documentation master file, created by 2 | sphinx-quickstart on Sun Oct 30 13:40:52 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../../README.rst 7 | 8 | Contents: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | logwrap 14 | PrettyFormat 15 | LogOnAccess 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /doc/source/logwrap.rst: -------------------------------------------------------------------------------- 1 | .. logwrap function and LogWrap class description. 2 | 3 | API: Decorators: `LogWrap` class and `logwrap` function. 4 | ======================================================== 5 | 6 | .. py:module:: logwrap 7 | .. py:currentmodule:: logwrap 8 | 9 | .. py:class:: LogWrap 10 | 11 | Log function calls and return values. 12 | 13 | .. versionadded:: 2.2.0 14 | 15 | .. py:method:: __init__(*, log=None, log_level=logging.DEBUG, exc_level=logging.ERROR, max_indent=20, max_iter=0, blacklisted_names=None, blacklisted_exceptions=None, log_call_args=True, log_call_args_on_exc=True, log_traceback=True, log_result_obj=True, ) 16 | 17 | :param log: logger object for decorator, by default trying to use logger from target module. Fallback: 'logwrap' 18 | :type log: logging.Logger | None 19 | :param log_level: log level for successful calls 20 | :type log_level: int 21 | :param exc_level: log level for exception cases 22 | :type exc_level: int 23 | :param max_indent: maximum indent before classic `repr()` call. 24 | :type max_indent: int 25 | :param max_iter: maximum number of elements to log from iterable 26 | :type max_iter: int 27 | :param blacklisted_names: Blacklisted argument names. 28 | Arguments with this names will be skipped in log. 29 | :type blacklisted_names: Iterable[str] | None 30 | :param blacklisted_exceptions: list of exception, 31 | which should be re-raised without 32 | producing traceback and text log record. 33 | :type blacklisted_exceptions: Iterable[type[Exception]] | None 34 | :param log_call_args: log call arguments before executing wrapped function. 35 | :type log_call_args: bool 36 | :param log_call_args_on_exc: log call arguments if exception raised. 37 | :type log_call_args_on_exc: bool 38 | :param log_traceback: log traceback on exception in addition to failure info 39 | :type log_traceback: bool 40 | :param log_result_obj: log result of function call. 41 | :type log_result_obj: bool 42 | 43 | .. versionchanged:: 3.3.0 Extract func from log and do not use Union. 44 | .. versionchanged:: 3.3.0 Deprecation of `*args` 45 | .. versionchanged:: 4.0.0 Drop of `*args` 46 | .. versionchanged:: 5.1.0 log_traceback parameter 47 | .. versionchanged:: 8.0.0 pick up logger from target module if possible 48 | .. versionchanged:: 9.0.0 Only LogWrap instance act as decorator 49 | .. versionchanged:: 11.1.0 max_iter parameter 50 | 51 | .. py:method:: pre_process_param(self, arg) 52 | 53 | Process parameter for the future logging. 54 | 55 | :param arg: bound parameter 56 | :type arg: BoundParameter 57 | :return: value, value override for logging or None if argument should not be logged. 58 | :rtype: BoundParameter | tuple[BoundParameter, typing.Any] | None 59 | 60 | Override this method if some modifications required for parameter value before logging 61 | 62 | .. versionadded:: 3.3.0 63 | 64 | .. py:method:: post_process_param(self, arg, arg_repr) 65 | 66 | Process parameter for the future logging. 67 | 68 | :param arg: bound parameter 69 | :type arg: BoundParameter 70 | :param arg_repr: repr for value 71 | :type arg_repr: str 72 | :return: processed repr for value 73 | :rtype: str 74 | 75 | Override this method if some modifications required for result of repr() over parameter 76 | 77 | .. versionadded:: 3.3.0 78 | 79 | .. note:: Attributes/properties names the same as argument names and changes 80 | the same fields. 81 | 82 | .. py:attribute:: log_level 83 | .. py:attribute:: exc_level 84 | .. py:attribute:: max_indent 85 | .. py:attribute:: max_iter 86 | .. py:attribute:: blacklisted_names 87 | 88 | ``list[str]``, modified via mutability 89 | .. py:attribute:: blacklisted_exceptions 90 | 91 | ``list[type[Exception]]``, modified via mutability 92 | .. py:attribute:: log_call_args 93 | .. py:attribute:: log_call_args_on_exc 94 | .. py:attribute:: log_traceback 95 | .. py:attribute:: log_result_obj 96 | 97 | .. py:method:: __call__(func) 98 | 99 | Decorator entry-point. Logic is stored separately and load depends on python version. 100 | 101 | :return: Decorated function. On python 3.3+ awaitable is supported. 102 | :rtype: Callable | Awaitable 103 | 104 | 105 | .. py:function:: logwrap(func=None, *, log=None, log_level=logging.DEBUG, exc_level=logging.ERROR, max_indent=20, max_iter=0, blacklisted_names=None, blacklisted_exceptions=None, log_call_args=True, log_call_args_on_exc=True, log_traceback=True, log_result_obj=True, ) 106 | 107 | Log function calls and return values. 108 | 109 | :param func: function to wrap 110 | :type func: None | Callable 111 | :param log: logger object for decorator, by default trying to use logger from target module. Fallback: 'logwrap' 112 | :type log: logging.Logger | None 113 | :param log_level: log level for successful calls 114 | :type log_level: int 115 | :param exc_level: log level for exception cases 116 | :type exc_level: int 117 | :param max_indent: maximum indent before classic `repr()` call. 118 | :type max_indent: int 119 | :param max_iter: maximum number of elements to log from iterable 120 | :type max_iter: int 121 | :param blacklisted_names: Blacklisted argument names. Arguments with this names will be skipped in log. 122 | :type blacklisted_names: Iterable[str] | None 123 | :param blacklisted_exceptions: list of exceptions, which should be re-raised 124 | without producing traceback and text log record. 125 | :type blacklisted_exceptions: Iterable[type[Exception]] | None 126 | :param log_call_args: log call arguments before executing wrapped function. 127 | :type log_call_args: bool 128 | :param log_call_args_on_exc: log call arguments if exception raised. 129 | :type log_call_args_on_exc: bool 130 | :param log_traceback: log traceback on exception in addition to failure info 131 | :type log_traceback: bool 132 | :param log_result_obj: log result of function call. 133 | :type log_result_obj: bool 134 | :return: built real decorator. 135 | :rtype: LogWrap | Callable[..., Awaitable[typing.Any] | typing.Any] 136 | 137 | .. versionchanged:: 3.3.0 Extract func from log and do not use Union. 138 | .. versionchanged:: 3.3.0 Deprecation of `*args` 139 | .. versionchanged:: 4.0.0 Drop of `*args` 140 | .. versionchanged:: 5.1.0 log_traceback parameter 141 | .. versionchanged:: 8.0.0 pick up logger from target module if possible 142 | .. versionchanged:: 9.0.0 Only LogWrap instance act as decorator 143 | .. versionchanged:: 11.1.0 max_iter parameter 144 | 145 | 146 | .. py:class:: BoundParameter(inspect.Parameter) 147 | 148 | Parameter-like object store BOUND with value parameter. 149 | .. versionchanged:: 5.3.1 subclass inspect.Parameter 150 | 151 | .. versionadded:: 3.3.0 152 | 153 | .. py:method:: __init__(self, parameter, value=Parameter.empty) 154 | 155 | Parameter-like object store BOUND with value parameter. 156 | 157 | :param parameter: parameter from signature 158 | :type parameter: ``inspect.Parameter`` 159 | :param value: parameter real value 160 | :type value: typing.Any 161 | :raises ValueError: No default value and no value 162 | 163 | .. py:attribute:: parameter 164 | 165 | Parameter object. 166 | 167 | :rtype: BoundParameter 168 | 169 | .. py:attribute:: value 170 | 171 | Parameter value. 172 | 173 | :rtype: typing.Any 174 | 175 | .. py:method:: __str__(self) 176 | 177 | String representation. 178 | 179 | :rtype: ``str`` 180 | 181 | 182 | .. py:function:: bind_args_kwargs(sig, *args, **kwargs) 183 | 184 | Bind `*args` and `**kwargs` to signature and get Bound Parameters. 185 | 186 | :param sig: source signature 187 | :type sig: inspect.Signature 188 | :return: Iterator for bound parameters with all information about it 189 | :rtype: list[BoundParameter] 190 | 191 | .. versionadded:: 3.3.0 192 | .. versionchanged:: 5.3.1 return list 193 | -------------------------------------------------------------------------------- /flake8_requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | flake8-pyi 3 | flake8-async 4 | flake8-datetimez 5 | flake8-type-checking 6 | flake8-bandit 7 | flake8-builtins 8 | flake8-bugbear 9 | flake8-debugger 10 | flake8-executable 11 | flake8-implicit-str-concat 12 | flake8-simplify 13 | flake8-comprehensions 14 | flake8-import-conventions 15 | pep8-naming 16 | flake8-docstrings 17 | -------------------------------------------------------------------------------- /logwrap/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 - 2021 Alexey Stepanov aka penguinolog 2 | 3 | # Copyright 2016 Mirantis, Inc. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """logwrap module. 18 | 19 | Contents: 'logwrap', 'pretty_repr', 'pretty_str' 20 | 21 | Original code was made for Mirantis Inc by Alexey Stepanov, 22 | later it has been reworked and extended for support of special cases. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from ._version import __version__ 28 | from ._version import __version_tuple__ 29 | from .log_on_access import LogOnAccess 30 | from .log_wrap import BoundParameter 31 | from .log_wrap import LogWrap 32 | from .log_wrap import bind_args_kwargs 33 | from .log_wrap import logwrap 34 | from .repr_utils import PrettyFormat 35 | from .repr_utils import PrettyRepr 36 | from .repr_utils import PrettyStr 37 | from .repr_utils import pretty_repr 38 | from .repr_utils import pretty_str 39 | 40 | __all__ = ( 41 | "BoundParameter", 42 | "LogOnAccess", 43 | "LogWrap", 44 | "PrettyFormat", 45 | "PrettyRepr", 46 | "PrettyStr", 47 | "__version__", 48 | "__version_tuple__", 49 | "bind_args_kwargs", 50 | "logwrap", 51 | "pretty_repr", 52 | "pretty_str", 53 | ) 54 | 55 | __author__ = "Aleksei Stepanov" 56 | __author_email__ = "penguinolog@gmail.com" 57 | __maintainers__ = { 58 | "Aleksei Stepanov": "penguinolog@gmail.com", 59 | "Antonio Esposito": "esposito.cloud@gmail.com", 60 | "Dennis Dmitriev": "dis-xcom@gmail.com", 61 | } 62 | __url__ = "https://github.com/python-useful-helpers/logwrap" 63 | __description__ = "Decorator for logging function arguments and return value by human-readable way" 64 | __license__ = "Apache License, Version 2.0" 65 | -------------------------------------------------------------------------------- /logwrap/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 - 2021 Alexey Stepanov aka penguinolog 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | """Global constants.""" 15 | 16 | VALID_LOGGER_NAMES = ("LOGGER", "LOG", "logger", "log", "_logger", "_log") 17 | -------------------------------------------------------------------------------- /logwrap/log_on_access.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - 2021 Alexey Stepanov aka penguinolog 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | """Property with logging on successful get/set/delete or failure.""" 15 | 16 | from __future__ import annotations 17 | 18 | import inspect 19 | import os 20 | import sys 21 | import time 22 | import traceback 23 | import typing 24 | from logging import DEBUG 25 | from logging import Logger 26 | from logging import getLogger 27 | 28 | from logwrap import repr_utils 29 | from logwrap.constants import VALID_LOGGER_NAMES 30 | 31 | if typing.TYPE_CHECKING: 32 | from collections.abc import Callable 33 | 34 | __all__ = ("LogOnAccess",) 35 | 36 | _LOGGER: Logger = getLogger(__name__) 37 | _CURRENT_FILE = os.path.abspath(__file__) 38 | _OwnerT = typing.TypeVar("_OwnerT") 39 | _ReturnT = typing.TypeVar("_ReturnT") 40 | 41 | 42 | class LogOnAccess(property, typing.Generic[_OwnerT, _ReturnT]): 43 | """Property with logging on successful get/set/delete or failure. 44 | 45 | Usage examples: 46 | 47 | >>> import logging 48 | >>> import io 49 | 50 | >>> log = io.StringIO() 51 | >>> logging.basicConfig(level=logging.DEBUG, stream=log) 52 | 53 | >>> class Test: 54 | ... def __init__(self, val="ok"): 55 | ... self.val = val 56 | ... 57 | ... def __repr__(self): 58 | ... return f"{self.__class__.__name__}(val={self.val})" 59 | ... 60 | ... @LogOnAccess 61 | ... def ok(self): 62 | ... return self.val 63 | ... 64 | ... @ok.setter 65 | ... def ok(self, val): 66 | ... self.val = val 67 | ... 68 | ... @ok.deleter 69 | ... def ok(self): 70 | ... self.val = "" 71 | ... 72 | ... @LogOnAccess 73 | ... def fail_get(self): 74 | ... raise RuntimeError() 75 | ... 76 | ... @LogOnAccess 77 | ... def fail_set_del(self): 78 | ... return self.val 79 | ... 80 | ... @fail_set_del.setter 81 | ... def fail_set_del(self, value): 82 | ... raise ValueError(value) 83 | ... 84 | ... @fail_set_del.deleter 85 | ... def fail_set_del(self): 86 | ... raise RuntimeError() 87 | 88 | >>> test = Test() 89 | >>> test.ok 90 | 'ok' 91 | >>> test.ok = "OK" 92 | >>> del test.ok 93 | >>> test.ok = "fail_get" 94 | 95 | >>> test.fail_get 96 | Traceback (most recent call last): 97 | ... 98 | RuntimeError 99 | 100 | >>> test.ok = "fail_set_del" 101 | >>> test.fail_set_del 102 | 'fail_set_del' 103 | 104 | >>> test.fail_set_del = "fail" 105 | Traceback (most recent call last): 106 | ... 107 | ValueError: fail 108 | 109 | >>> del test.fail_set_del 110 | Traceback (most recent call last): 111 | ... 112 | RuntimeError 113 | 114 | >>> test.fail_set_del 115 | 'fail_set_del' 116 | 117 | >>> logs = log.getvalue().splitlines() 118 | >>> # Getter 119 | >>> logs[0] 120 | 'DEBUG:log_on_access:Request: Test(val=ok).ok' 121 | >>> logs[1] 122 | "DEBUG:log_on_access:Done at 0.000s: Test(val=ok).ok -> 'ok'" 123 | >>> # Setter 124 | >>> logs[2] 125 | "DEBUG:log_on_access:Request: Test(val=ok).ok = 'OK'" 126 | >>> logs[3] 127 | "DEBUG:log_on_access:Done at 0.000s: Test(val=ok).ok = 'OK'" 128 | >>> # Deleter 129 | >>> logs[4] 130 | 'DEBUG:log_on_access:Request: del Test(val=OK).ok' 131 | >>> logs[5] 132 | 'DEBUG:log_on_access:Done at 0.000s: del Test(val=OK).ok' 133 | >>> # Setter without getter 134 | >>> logs[6] 135 | "DEBUG:log_on_access:Request: Test(val=).ok = 'fail_get'" 136 | >>> logs[7] 137 | "DEBUG:log_on_access:Done at 0.000s: Test(val=).ok = 'fail_get'" 138 | >>> # Failed getter (not set) 139 | >>> logs[8] 140 | 'DEBUG:log_on_access:Request: Test(val=fail_get).fail_get' 141 | >>> logs[9] 142 | 'DEBUG:log_on_access:Failed after 0.000s: Test(val=fail_get).fail_get' 143 | >>> logs[10] 144 | 'Traceback (most recent call last):' 145 | 146 | .. versionadded:: 6.1.0 147 | """ 148 | 149 | __slots__ = ( 150 | "__dict__", 151 | "__exc_level", 152 | "__log_before", 153 | "__log_failure", 154 | "__log_level", 155 | "__log_object_repr", 156 | "__log_success", 157 | "__log_traceback", 158 | "__logger", 159 | "__max_indent", 160 | "__name", 161 | "__override_name", 162 | "__owner", 163 | ) 164 | 165 | def __init__( 166 | self, 167 | fget: Callable[[_OwnerT], _ReturnT] | None = None, 168 | fset: Callable[[_OwnerT, _ReturnT], None] | None = None, 169 | fdel: Callable[[_OwnerT], None] | None = None, 170 | doc: str | None = None, 171 | *, 172 | # Extended settings start 173 | logger: Logger | str | None = None, 174 | log_object_repr: bool = True, 175 | log_level: int = DEBUG, 176 | exc_level: int = DEBUG, 177 | log_before: bool = True, 178 | log_success: bool = True, 179 | log_failure: bool = True, 180 | log_traceback: bool = True, 181 | override_name: str | None = None, 182 | max_indent: int = 20, 183 | max_iter: int = 0, 184 | ) -> None: 185 | """Advanced property main entry point. 186 | 187 | :param fget: normal getter. 188 | :type fget: Callable[[_OwnerT], _ReturnT] | None 189 | :param fset: normal setter. 190 | :type fset: Callable[[_OwnerT, _ReturnT], None] | None 191 | :param fdel: normal deleter. 192 | :type fdel: Callable[[_OwnerT], None] | None 193 | :param doc: docstring override 194 | :type doc: str | None 195 | :param logger: logger instance or name to use as override 196 | :type logger: logging.Logger | str | None 197 | :param log_object_repr: use `repr` over object to describe owner if True else owner class name and id 198 | :type log_object_repr: bool 199 | :param log_level: log level for successful operations 200 | :type log_level: int 201 | :param exc_level: log level for exceptions 202 | :type exc_level: int 203 | :param log_before: log before operation 204 | :type log_before: bool 205 | :param log_success: log successful operations 206 | :type log_success: bool 207 | :param log_failure: log exceptions 208 | :type log_failure: bool 209 | :param log_traceback: Log traceback on exceptions 210 | :type log_traceback: bool 211 | :param override_name: override property name if not None else use getter/setter/deleter name 212 | :type override_name: str | None 213 | :param max_indent: maximal indent before classic repr() call 214 | :type max_indent: int 215 | :param max_iter: maximal number of items to display in iterables 216 | :type max_iter: int 217 | """ 218 | super().__init__(fget=fget, fset=fset, fdel=fdel, doc=doc) 219 | 220 | if logger is None or isinstance(logger, Logger): 221 | self.__logger: Logger | None = logger 222 | else: 223 | self.__logger = getLogger(logger) 224 | 225 | self.__log_object_repr: bool = log_object_repr 226 | self.__log_level: int = log_level 227 | self.__exc_level: int = exc_level 228 | self.__log_before: bool = log_before 229 | self.__log_success: bool = log_success 230 | self.__log_failure: bool = log_failure 231 | self.__log_traceback: bool = log_traceback 232 | self.__override_name: str | None = override_name 233 | self.__max_indent: int = max_indent 234 | self.__max_iter: int = max_iter 235 | self.__name: str = "" 236 | self.__owner: type[_OwnerT] | None = None 237 | 238 | def __set_name__(self, owner: type[_OwnerT] | None, name: str) -> None: 239 | """Set __name__ and __objclass__ property. 240 | 241 | :param owner: owner class, where descriptor applied 242 | :type owner: type[_OwnerT] | None 243 | :param name: descriptor name 244 | :type name: str 245 | """ 246 | self.__owner = owner 247 | self.__name = name 248 | 249 | @property 250 | def __objclass__(self) -> type[_OwnerT] | None: # pragma: no cover # noqa: PLW3201,RUF100 251 | """Read-only owner. 252 | 253 | :return: property owner class 254 | :rtype: type[_OwnerT] | None 255 | """ 256 | return self.__owner 257 | 258 | @property 259 | def __traceback(self) -> str: 260 | """Get outer traceback text for logging. 261 | 262 | :return: traceback without decorator internals if traceback logging enabled else empty line 263 | :rtype: str 264 | """ 265 | if not self.log_traceback: 266 | return "" 267 | exc_info = sys.exc_info() 268 | stack: traceback.StackSummary = traceback.extract_stack() 269 | full_tb: list[traceback.FrameSummary] = [elem for elem in stack if elem.filename != _CURRENT_FILE] 270 | exc_line: list[str] = traceback.format_exception_only(*exc_info[:2]) 271 | # Make standard traceback string 272 | return "\nTraceback (most recent call last):\n" + "".join(traceback.format_list(full_tb)) + "".join(exc_line) 273 | 274 | def __get_obj_source(self, instance: _OwnerT, owner: type[_OwnerT] | None = None) -> str: 275 | """Get object repr block. 276 | 277 | :param instance: object instance 278 | :type instance: typing.Any 279 | :param owner: object class (available for getter usage only) 280 | :type owner: type[_OwnerT] | None 281 | :return: repr of object if it not disabled else repr placeholder 282 | :rtype: str 283 | """ 284 | if self.log_object_repr: 285 | return repr_utils.pretty_repr(instance, max_indent=self.max_indent, max_iter=self.max_iter) 286 | if owner is not None: 287 | return f"<{owner.__name__}() at 0x{id(instance):X}>" 288 | if self.__objclass__ is not None: 289 | return f"<{self.__objclass__.__name__}() at 0x{id(instance):X}>" 290 | return f"<{instance.__class__.__name__}() at 0x{id(instance):X}>" 291 | 292 | def _get_logger_for_instance(self, instance: _OwnerT) -> Logger: 293 | """Get logger for log calls. 294 | 295 | :param instance: Owner class instance. Filled only if instance created, else None. 296 | :type instance: _OwnerT | None 297 | :return: logger instance 298 | :rtype: logging.Logger 299 | """ 300 | if self.logger is not None: 301 | return self.logger 302 | for logger_name in VALID_LOGGER_NAMES: 303 | logger_candidate = getattr(instance, logger_name, None) 304 | if isinstance(logger_candidate, Logger): 305 | return logger_candidate 306 | instance_module = inspect.getmodule(instance) 307 | for logger_name in VALID_LOGGER_NAMES: 308 | logger_candidate = getattr(instance_module, logger_name, None) 309 | if isinstance(logger_candidate, Logger): 310 | return logger_candidate 311 | return _LOGGER 312 | 313 | @typing.overload 314 | def __get__( 315 | self, 316 | instance: None, 317 | owner: type[_OwnerT] | None = None, 318 | ) -> typing.NoReturn: 319 | """Get descriptor. 320 | 321 | :param instance: Owner class instance. Filled only if instance created, else None. 322 | :type instance: _OwnerT | None 323 | :param owner: Owner class for property. 324 | :return: getter call result if getter presents 325 | :rtype: typing.Any 326 | :raises AttributeError: Getter is not available 327 | :raises Exception: Something goes wrong 328 | """ 329 | 330 | @typing.overload 331 | def __get__( 332 | self, 333 | instance: _OwnerT, 334 | owner: type[_OwnerT] | None = None, 335 | ) -> _ReturnT: 336 | """Get descriptor. 337 | 338 | :param instance: Owner class instance. Filled only if instance created, else None. 339 | :type instance: _OwnerT | None 340 | :param owner: Owner class for property. 341 | :return: getter call result if getter presents 342 | :rtype: typing.Any 343 | :raises AttributeError: Getter is not available 344 | :raises Exception: Something goes wrong 345 | """ 346 | 347 | def __get__( 348 | self, 349 | instance: _OwnerT | None, 350 | owner: type[_OwnerT] | None = None, 351 | ) -> _ReturnT: 352 | """Get descriptor. 353 | 354 | :param instance: Owner class instance. Filled only if instance created, else None. 355 | :type instance: _OwnerT | None 356 | :param owner: Owner class for property. 357 | :return: getter call result if getter presents 358 | :rtype: typing.Any 359 | :raises AttributeError: Getter is not available 360 | :raises Exception: Something goes wrong 361 | """ 362 | if instance is None or self.fget is None: 363 | raise AttributeError() 364 | 365 | source: str = self.__get_obj_source(instance, owner) 366 | logger: Logger = self._get_logger_for_instance(instance) 367 | 368 | timestamp: float = time.time() 369 | try: 370 | if self.log_before: 371 | logger.log(self.log_level, f"Request: {source}.{self.__name__}") 372 | result: _ReturnT = super().__get__(instance, owner) 373 | if self.log_success: 374 | logger.log( 375 | self.log_level, 376 | f"Done at {time.time() - timestamp:.03f}s: " 377 | f"{source}.{self.__name__} -> {repr_utils.pretty_repr(result)}", 378 | ) 379 | except Exception: 380 | if self.log_failure: 381 | logger.log( 382 | self.exc_level, 383 | f"Failed after {time.time() - timestamp:.03f}s: {source}.{self.__name__}{self.__traceback}", 384 | exc_info=False, 385 | ) 386 | raise 387 | return result 388 | 389 | def __set__(self, instance: _OwnerT, value: _ReturnT) -> None: 390 | """Set descriptor. 391 | 392 | :param instance: Owner class instance. Filled only if instance created, else None. 393 | :type instance: _OwnerT | None 394 | :param value: Value for setter 395 | :raises AttributeError: Setter is not available 396 | :raises Exception: Something goes wrong 397 | """ 398 | if self.fset is None: 399 | raise AttributeError() 400 | 401 | source: str = self.__get_obj_source(instance) 402 | logger: Logger = self._get_logger_for_instance(instance) 403 | 404 | timestamp: float = time.time() 405 | try: 406 | if self.log_before: 407 | logger.log(self.log_level, f"Request: {source}.{self.__name__} = {repr_utils.pretty_repr(value)}") 408 | super().__set__(instance, value) 409 | if self.log_success: 410 | logger.log( 411 | self.log_level, 412 | f"Done at {time.time() - timestamp:.03f}s: " 413 | f"{source}.{self.__name__} = {repr_utils.pretty_repr(value)}", 414 | ) 415 | except Exception: 416 | if self.log_failure: 417 | logger.log( 418 | self.exc_level, 419 | f"Failed after {time.time() - timestamp:.03f}s: " 420 | f"{source}.{self.__name__} = {repr_utils.pretty_repr(value)}{self.__traceback}", 421 | exc_info=False, 422 | ) 423 | raise 424 | 425 | def __delete__(self, instance: _OwnerT) -> None: 426 | """Delete descriptor. 427 | 428 | :param instance: Owner class instance. Filled only if instance created, else None. 429 | :type instance: _OwnerT | None 430 | :raises AttributeError: Deleter is not available 431 | :raises Exception: Something goes wrong 432 | """ 433 | if self.fdel is None: 434 | raise AttributeError() 435 | 436 | source: str = self.__get_obj_source(instance) 437 | logger: Logger = self._get_logger_for_instance(instance) 438 | 439 | timestamp: float = time.time() 440 | try: 441 | if self.log_before: 442 | logger.log(self.log_level, f"Request: del {source}.{self.__name__}") 443 | super().__delete__(instance) 444 | if self.log_success: 445 | logger.log(self.log_level, f"Done at {time.time() - timestamp:.03f}s: del {source}.{self.__name__}") 446 | except Exception: 447 | if self.log_failure: 448 | logger.log( 449 | self.exc_level, 450 | f"Failed after {time.time() - timestamp:.03f}s: del {source}.{self.__name__}{self.__traceback}", 451 | exc_info=False, 452 | ) 453 | raise 454 | 455 | @property 456 | def logger(self) -> Logger | None: 457 | """Logger instance to use as override. 458 | 459 | :return: logger instance if set 460 | :rtype: logging.Logger | None 461 | """ 462 | return self.__logger 463 | 464 | @logger.setter 465 | def logger(self, logger: Logger | str | None) -> None: 466 | """Logger instance to use as override. 467 | 468 | :param logger: logger instance, logger name or None if override disable required 469 | :type logger: logging.Logger | str | None 470 | """ 471 | if logger is None or isinstance(logger, Logger): 472 | self.__logger = logger 473 | else: 474 | self.__logger = getLogger(logger) 475 | 476 | @property 477 | def log_object_repr(self) -> bool: 478 | """Use `repr` over object to describe owner if True else owner class name and id. 479 | 480 | :return: switch state 481 | :rtype: bool 482 | """ 483 | return self.__log_object_repr 484 | 485 | @log_object_repr.setter 486 | def log_object_repr(self, value: bool) -> None: 487 | """Use `repr` over object to describe owner if True else owner class name and id. 488 | 489 | :param value: switch state 490 | :type value: bool 491 | """ 492 | self.__log_object_repr = value 493 | 494 | @property 495 | def log_level(self) -> int: 496 | """Log level for successful operations. 497 | 498 | :return: log level 499 | :rtype: int 500 | """ 501 | return self.__log_level 502 | 503 | @log_level.setter 504 | def log_level(self, value: int) -> None: 505 | """Log level for successful operations. 506 | 507 | :param value: log level 508 | :type value: int 509 | """ 510 | self.__log_level = value 511 | 512 | @property 513 | def exc_level(self) -> int: 514 | """Log level for exceptions. 515 | 516 | :return: log level 517 | :rtype: int 518 | """ 519 | return self.__exc_level 520 | 521 | @exc_level.setter 522 | def exc_level(self, value: int) -> None: 523 | """Log level for exceptions. 524 | 525 | :param value: log level 526 | :type value: int 527 | """ 528 | self.__exc_level = value 529 | 530 | @property 531 | def log_before(self) -> bool: 532 | """Log before operation. 533 | 534 | :return: switch state 535 | :rtype: bool 536 | """ 537 | return self.__log_before 538 | 539 | @log_before.setter 540 | def log_before(self, value: bool) -> None: 541 | """Log before operations. 542 | 543 | :param value: switch state 544 | :type value: bool 545 | """ 546 | self.__log_before = value 547 | 548 | @property 549 | def log_success(self) -> bool: 550 | """Log successful operations. 551 | 552 | :return: switch state 553 | :rtype: bool 554 | """ 555 | return self.__log_success 556 | 557 | @log_success.setter 558 | def log_success(self, value: bool) -> None: 559 | """Log successful operations. 560 | 561 | :param value: switch state 562 | :type value: bool 563 | """ 564 | self.__log_success = value 565 | 566 | @property 567 | def log_failure(self) -> bool: 568 | """Log exceptions. 569 | 570 | :return: switch state 571 | :rtype: bool 572 | """ 573 | return self.__log_failure 574 | 575 | @log_failure.setter 576 | def log_failure(self, value: bool) -> None: 577 | """Log exceptions. 578 | 579 | :param value: switch state 580 | :type value: bool 581 | """ 582 | self.__log_failure = value 583 | 584 | @property 585 | def log_traceback(self) -> bool: 586 | """Log traceback on exceptions. 587 | 588 | :return: switch state 589 | :rtype: bool 590 | """ 591 | return self.__log_traceback 592 | 593 | @log_traceback.setter 594 | def log_traceback(self, value: bool) -> None: 595 | """Log traceback on exceptions. 596 | 597 | :param value: switch state 598 | :type value: bool 599 | """ 600 | self.__log_traceback = value 601 | 602 | @property 603 | def override_name(self) -> str | None: 604 | """Override property name if not None else use getter/setter/deleter name. 605 | 606 | :return: property name override 607 | :rtype: str | None 608 | """ 609 | return self.__override_name 610 | 611 | @override_name.setter 612 | def override_name(self, name: str | None) -> None: 613 | """Override property name if not None else use getter/setter/deleter name. 614 | 615 | :param name: property name override 616 | :type name: str | None 617 | """ 618 | self.__override_name = name 619 | 620 | @property 621 | def max_indent(self) -> int: 622 | """Max indent during repr. 623 | 624 | :return: maximum indent before classic `repr()` call. 625 | :rtype: int 626 | """ 627 | return self.__max_indent 628 | 629 | @max_indent.setter 630 | def max_indent(self, value: int) -> None: 631 | """Max indent during repr. 632 | 633 | :param value: maximum indent before classic `repr()` call. 634 | :type value: int 635 | """ 636 | self.__max_indent = value 637 | 638 | @property 639 | def max_iter(self) -> int: 640 | """Max number of items in iterables during repr. 641 | 642 | :return: maximum iter before classic `repr()` call. 643 | :rtype: int 644 | """ 645 | return self.__max_iter 646 | 647 | @max_iter.setter 648 | def max_iter(self, value: int) -> None: 649 | """Max number of items in iterables during repr. 650 | 651 | :param value: maximum iter before classic `repr()` call. 652 | :type value: int 653 | """ 654 | self.__max_iter = value 655 | 656 | @property 657 | def __name__(self) -> str: # type: ignore[override] # noqa: A003,PLW3201,RUF100 658 | """Name getter. 659 | 660 | :return: attribute name (may be overridden) 661 | :rtype: str 662 | """ 663 | if self.override_name: 664 | return self.override_name 665 | if self.__name: 666 | return self.__name 667 | if self.fget is not None: 668 | return self.fget.__name__ 669 | if self.fset is not None: 670 | return self.fset.__name__ 671 | if self.fdel is not None: 672 | return self.fdel.__name__ 673 | return "" 674 | 675 | 676 | if __name__ == "__main__": 677 | import doctest 678 | 679 | doctest.testmod(verbose=True) 680 | -------------------------------------------------------------------------------- /logwrap/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-useful-helpers/logwrap/6ceee343c6184f7101078c78e976b0851d9da260/logwrap/py.typed -------------------------------------------------------------------------------- /mypy_requirements.txt: -------------------------------------------------------------------------------- 1 | mypy 2 | rich 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # Minimum requirements for the build system to execute. 3 | # PEP 508 specifications for PEP 518. 4 | requires = [ 5 | "setuptools >= 61.0.0", 6 | "setuptools_scm[toml]>=6.2", 7 | "wheel", 8 | ] 9 | build-backend="setuptools.build_meta" 10 | 11 | [project] 12 | name = "logwrap" 13 | description = "Decorator for logging function arguments and return value by human-readable way" 14 | requires-python = ">=3.9.0" 15 | keywords = ["logging", "debugging", "development"] 16 | license={text="Apache-2.0"} # Use SPDX classifier 17 | readme = {file = "README.rst", content-type = "text/x-rst"} 18 | authors=[{name="Aleksei Stepanov", email="penguinolog@gmail.com"}] 19 | maintainers=[ 20 | {name="Aleksei Stepanov", email="penguinolog@gmail.com"}, 21 | {name="Antonio Esposito", email="esposito.cloud@gmail.com"}, 22 | {name="Dennis Dmitriev", email="dis-xcom@gmail.com"}, 23 | ] 24 | dynamic = ["classifiers", "version"] # "dependencies", 25 | 26 | [project.urls] 27 | "Documentation" = "https://logwrap.readthedocs.io/" 28 | "Repository" = "https://github.com/python-useful-helpers/logwrap" 29 | "Bug Tracker" = "https://github.com/python-useful-helpers/logwrap/issues" 30 | 31 | [tool.setuptools.package-data] 32 | logwrap=[ 33 | "py.typed", 34 | "*.pyi", 35 | ] 36 | 37 | [tool.setuptools] 38 | zip-safe = false 39 | 40 | [tool.setuptools.packages.find] 41 | exclude = [ 42 | "doc*", 43 | "examples", 44 | "test*", 45 | "bin", 46 | ".*" 47 | ] 48 | namespaces = false 49 | 50 | [tool.setuptools.dynamic] 51 | classifiers = {file = ["classifiers.txt"]} 52 | 53 | [tool.distutils.bdist_wheel] 54 | universal = 0 55 | 56 | [tool.setuptools_scm] 57 | write_to = "logwrap/_version.py" 58 | 59 | [tool.black] 60 | line-length = 120 61 | target-version = ["py39"] 62 | 63 | [tool.isort] 64 | line_length = 120 65 | multi_line_output = 3 66 | force_single_line = true 67 | 68 | [tool.doc8] 69 | max-line-length = 150 70 | 71 | [tool.pydocstyle] 72 | ignore = [ 73 | "D401", 74 | "D202", 75 | "D203", 76 | "D213" 77 | ] 78 | # First line should be in imperative mood; try rephrasing 79 | # No blank lines allowed after function docstring 80 | # 1 blank line required before class docstring 81 | # Multi-line docstring summary should start at the second line 82 | match = "(?!_version|test_)*.py" 83 | 84 | [tool.mypy] 85 | strict = true 86 | warn_unused_configs = true 87 | warn_redundant_casts = true 88 | show_error_context = true 89 | show_column_numbers = true 90 | show_error_codes = true 91 | pretty = true 92 | 93 | [tool.pytest.ini_options] 94 | minversion = "6.0" 95 | addopts = "-vvv -s -p no:django -p no:ipdb" 96 | testpaths = ["test"] 97 | mock_use_standalone_module = false 98 | junit_family = "xunit2" 99 | asyncio_default_fixture_loop_scope = "function" 100 | 101 | [tool.coverage.run] 102 | omit = ["test/*"] 103 | branch = true 104 | 105 | [tool.coverage.report] 106 | exclude_lines = [ 107 | # Have to re-enable the standard pragma 108 | "pragma: no cover", 109 | 110 | # Don't complain about missing debug-only code: 111 | "def __repr__", 112 | 113 | # Don't complain if tests don't hit defensive assertion code: 114 | "raise NotImplementedError", 115 | 116 | # Exclude methods marked as abstract 117 | "@abstractmethod", 118 | 119 | # Exclude import statements 120 | "^from\b", 121 | "^import\b", 122 | 123 | # Exclude variable declarations that are executed when file is loaded 124 | "^[a-zA-Z_]+\b\\s=", 125 | 126 | # Code for static analysis is never covered: 127 | "if typing.TYPE_CHECKING:", 128 | 129 | # Fallback code with no installed deps is almost impossible to cover properly 130 | "except ImportError:", 131 | 132 | # Don't complain if non-runnable code isn't run: 133 | "if __name__ == .__main__.:", 134 | 135 | # OS Specific 136 | "if platform.system()", 137 | ] 138 | 139 | [tool.coverage.json] 140 | pretty_print = true 141 | 142 | [tool.ruff] 143 | line-length = 120 144 | output-format = "full" 145 | target-version = "py39" 146 | 147 | [tool.ruff.lint] 148 | extend-select = [ 149 | "E", 150 | "W", # also pycodestyle warnings 151 | "PYI", # flake8-pyi 152 | "ASYNC", # flake8-async 153 | "FA", # from __future__ import annotations 154 | "DTZ", # flake8-datetimez 155 | "SLOT", # flake8-slots 156 | "TC", # flake8-type-checking 157 | "S", # flake8-bandit 158 | "A", # flake8-builtins 159 | "B", "T10", "EXE", # flake8-bugbear, flake8-debugger, flake8-executable 160 | "ISC", # flake8-implicit-str-concat 161 | "RET", "SIM", "C4", # flake8-return, flake8-simplify, flake8-comprehensions 162 | "ICN", "PGH", # flake8-import-conventions, pygrep-hooks 163 | "TID", # flake8-tidy-imports 164 | "Q", # quotes 165 | "FLY", # Flynt 166 | "FURB", # Refurb 167 | "TRY", "UP", "I", "PL", "PERF", "RUF", # tryceratops, pyupgrade, isort, pylint + perflint, Ruff-specific 168 | ] 169 | extend-ignore = [ 170 | # refactor rules (too many statements/arguments/branches) 171 | "PLR0904", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR0917", "PLR2004", 172 | "PLR6301", # "maybe staticmethod" 173 | "RET504", # Unnecessary variable assignment before return statement 174 | "SIM108", # Use ternary operator, 175 | "TRY003", # long messages prepare outside, ... 176 | ] 177 | 178 | [tool.ruff.lint.isort] 179 | force-single-line = true 180 | known-third-party = [] 181 | 182 | [tool.ruff.lint.pylint] 183 | allow-dunder-method-names = ["__pretty_repr__", "__pretty_str__", "__rich_repr__"] 184 | 185 | [tool.ruff.lint.pydocstyle] 186 | convention = "pep257" 187 | 188 | [tool.ruff.format] 189 | docstring-code-format = true 190 | 191 | [tool.refurb] 192 | python_version = "3.9" 193 | enable_all = true 194 | ignore = ["FURB120"] 195 | 196 | [tool.pylint] 197 | extension-pkg-whitelist = ["lxml.etree"] 198 | ignore = ["CVS", "_version.py"] 199 | jobs = 0 200 | py-version = "3.9" 201 | 202 | load-plugins = [ 203 | "pylint.extensions.docparams", 204 | "pylint.extensions.docstyle", 205 | "pylint.extensions.overlapping_exceptions", 206 | "pylint.extensions.check_elif", 207 | "pylint.extensions.for_any_all", 208 | "pylint.extensions.code_style", 209 | "pylint.extensions.redefined_variable_type", 210 | "pylint.extensions.typing", 211 | "pylint.extensions.empty_comment", 212 | "pylint.extensions.dunder", 213 | ] 214 | 215 | enable = "all" 216 | disable = [ 217 | "locally-disabled", 218 | "file-ignored", 219 | "suppressed-message", 220 | "similarities", 221 | "too-many-ancestors", 222 | "too-few-public-methods", 223 | "too-many-public-methods", 224 | "too-many-return-statements", 225 | "too-many-branches", 226 | "too-many-arguments", 227 | "too-many-positional-arguments", 228 | "too-many-locals", 229 | "too-many-statements", 230 | "too-many-instance-attributes", 231 | "too-many-lines", 232 | "broad-except", 233 | "logging-fstring-interpolation", 234 | "logging-format-interpolation", 235 | "consider-using-assignment-expr", 236 | "invalid-name" 237 | ] 238 | max-line-length = 120 239 | reports = false 240 | 241 | [tool.pylint.dunder] 242 | good-dunder-names = [ 243 | "__pretty_repr__", 244 | "__pretty_str__", 245 | "__rich_repr__", 246 | "__objclass__", 247 | "__name__", 248 | ] 249 | 250 | [tool.pylint.parameter_documentation] 251 | accept-no-param-doc = true 252 | accept-no-raise-doc = false 253 | accept-no-return-doc = false 254 | accept-no-yields-doc = false 255 | 256 | # Possible choices: ['sphinx', 'epytext', 'google', 'numpy', 'default'] 257 | default-docstring-type = "default" 258 | -------------------------------------------------------------------------------- /pytest_requirements.txt: -------------------------------------------------------------------------------- 1 | asynctest 2 | pytest > 7.0 3 | pytest-cov 4 | pytest-mock 5 | pytest-asyncio 6 | # pytest-sugar 7 | coverage[toml]>=5.0 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-useful-helpers/logwrap/6ceee343c6184f7101078c78e976b0851d9da260/requirements.txt -------------------------------------------------------------------------------- /test/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | # Extend the `pyproject.toml` file in the parent directory. 3 | extend = "../pyproject.toml" 4 | 5 | [tool.ruff.lint] 6 | extend-ignore = [ 7 | "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004", # refactor rules (too many statements/arguments/branches) 8 | "PLW2901", # `Outer variable overwritten by inner target 9 | "RET504", # Unnecessary variable assignment before return statement 10 | "SIM108", # Use ternary operator, 11 | "TRY002", "TRY003", "TRY300", "TRY301", "TRY400", # do not raise `Exception`, long messages prepare outside, ... 12 | "PTH118", "PTH119", # `os.path.join()`, `os.path.basename()` should be replaced 13 | "S", 14 | ] 15 | 16 | [tool.ruff.lint.isort] 17 | known-first-party = ["logwrap"] 18 | -------------------------------------------------------------------------------- /test/test_log_on_access.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | """Tests for logwrap.LogOnAccess.""" 4 | 5 | import io 6 | import logging 7 | import unittest 8 | 9 | import logwrap 10 | 11 | VALUE = "ok" 12 | 13 | 14 | class TestLogOnAccess(unittest.TestCase): 15 | def setUp(self): 16 | """Preparation for tests.""" 17 | self.stream = io.StringIO() 18 | logging.getLogger().handlers.clear() 19 | logging.basicConfig(level=logging.DEBUG, stream=self.stream) 20 | 21 | def tearDown(self): 22 | """Revert modifications.""" 23 | logging.getLogger().handlers.clear() 24 | self.stream.close() 25 | 26 | def test_01_positive(self): 27 | # noinspection PyMissingOrEmptyDocstring 28 | class Target: 29 | def __init__(tself, val=VALUE): 30 | tself.val = val 31 | 32 | def __repr__(tself): 33 | return f"{tself.__class__.__name__}(val={tself.val})" 34 | 35 | @logwrap.LogOnAccess 36 | def ok(tself): 37 | return tself.val 38 | 39 | @ok.setter 40 | def ok(tself, val): 41 | tself.val = val 42 | 43 | @ok.deleter 44 | def ok(tself): 45 | tself.val = "" 46 | 47 | target = Target() 48 | self.assertEqual(target.ok, VALUE) 49 | logged = self.stream.getvalue().splitlines() 50 | self.assertEqual("DEBUG:logwrap.log_on_access:Request: Target(val=ok).ok", logged[0]) 51 | self.assertRegex( 52 | logged[1], 53 | rf"DEBUG:logwrap\.log_on_access:Done at (?:\d+\.\d{{3}})s: " 54 | rf"Target\(val=ok\)\.ok -> {logwrap.pretty_repr(VALUE)}", 55 | ) 56 | 57 | self.stream.seek(0) 58 | self.stream.truncate() 59 | 60 | target.ok = VALUE.upper() 61 | logged = self.stream.getvalue().splitlines() 62 | self.assertEqual( 63 | f"DEBUG:logwrap.log_on_access:Request: Target(val=ok).ok = {logwrap.pretty_repr(VALUE.upper())}", 64 | logged[0], 65 | ) 66 | self.assertRegex( 67 | logged[1], 68 | rf"DEBUG:logwrap\.log_on_access:Done at (?:\d+\.\d{{3}})s: " 69 | rf"Target\(val=ok\)\.ok = {logwrap.pretty_repr(VALUE.upper())}", 70 | ) 71 | 72 | self.assertEqual(target.ok, VALUE.upper()) 73 | 74 | self.stream.seek(0) 75 | self.stream.truncate() 76 | 77 | del target.ok 78 | logged = self.stream.getvalue().splitlines() 79 | self.assertEqual("DEBUG:logwrap.log_on_access:Request: del Target(val=OK).ok", logged[0]) 80 | self.assertRegex( 81 | logged[1], 82 | r"DEBUG:logwrap\.log_on_access:Done at (?:\d+\.\d{3})s: del Target\(val=OK\)\.ok", 83 | ) 84 | 85 | def test_02_positive_properties(self): 86 | # noinspection PyMissingOrEmptyDocstring 87 | class Target: 88 | def __init__(tself, val=VALUE): 89 | tself.val = val 90 | 91 | def __repr__(tself): 92 | return f"{tself.__class__.__name__}(val={tself.val})" 93 | 94 | @logwrap.LogOnAccess 95 | def ok(tself): 96 | return tself.val 97 | 98 | ok.log_level = logging.INFO 99 | ok.log_object_repr = False 100 | ok.override_name = "override" 101 | 102 | target = Target() 103 | 104 | self.assertEqual(target.ok, VALUE) 105 | logged = self.stream.getvalue().splitlines() 106 | self.assertEqual( 107 | f"INFO:logwrap.log_on_access:Request: .override", 108 | logged[0], 109 | ) 110 | self.assertRegex( 111 | logged[1], 112 | rf"INFO:logwrap\.log_on_access:Done at (?:\d+\.\d{{3}})s: " 113 | rf"\.override -> {logwrap.pretty_repr(VALUE)}", 114 | ) 115 | 116 | def test_03_positive_no_log(self): 117 | # noinspection PyMissingOrEmptyDocstring 118 | class Target: 119 | def __init__(tself, val=VALUE): 120 | tself.val = val 121 | 122 | def __repr__(tself): 123 | return f"{tself.__class__.__name__}(val={tself.val})" 124 | 125 | @logwrap.LogOnAccess 126 | def ok(tself): 127 | return tself.val 128 | 129 | ok.log_success = False 130 | ok.log_before = False 131 | 132 | target = Target() 133 | 134 | self.assertEqual(target.ok, VALUE) 135 | self.assertEqual(self.stream.getvalue(), "") 136 | 137 | def test_04_negative(self): 138 | # noinspection PyMissingOrEmptyDocstring 139 | class Target: 140 | def __repr__(tself): 141 | return f"{tself.__class__.__name__}()" 142 | 143 | @logwrap.LogOnAccess 144 | def ok(tself): 145 | raise AttributeError() 146 | 147 | @ok.setter 148 | def ok(tself, val): 149 | raise ValueError(val) 150 | 151 | @ok.deleter 152 | def ok(tself): 153 | raise RuntimeError() 154 | 155 | target = Target() 156 | 157 | with self.assertRaises(AttributeError): 158 | self.assertIsNone(target.ok) 159 | 160 | logged = self.stream.getvalue().splitlines() 161 | self.assertEqual("DEBUG:logwrap.log_on_access:Request: Target().ok", logged[0]) 162 | self.assertRegex( 163 | logged[1], 164 | r"DEBUG:logwrap\.log_on_access:Failed after (?:\d+\.\d{3})s: Target\(\)\.ok", 165 | ) 166 | self.assertEqual("Traceback (most recent call last):", logged[2]) 167 | 168 | self.stream.seek(0) 169 | self.stream.truncate() 170 | 171 | with self.assertRaises(ValueError): 172 | target.ok = VALUE 173 | 174 | logged = self.stream.getvalue().splitlines() 175 | self.assertEqual( 176 | f"DEBUG:logwrap.log_on_access:Request: Target().ok = {logwrap.pretty_repr(VALUE)}", 177 | logged[0], 178 | ) 179 | self.assertRegex( 180 | logged[1], 181 | rf"DEBUG:logwrap\.log_on_access:Failed after (?:\d+\.\d{{3}})s: " 182 | rf"Target\(\)\.ok = {logwrap.pretty_repr(VALUE)}", 183 | ) 184 | self.assertEqual("Traceback (most recent call last):", logged[2]) 185 | 186 | self.stream.seek(0) 187 | self.stream.truncate() 188 | 189 | with self.assertRaises(RuntimeError): 190 | del target.ok 191 | 192 | logged = self.stream.getvalue().splitlines() 193 | self.assertEqual("DEBUG:logwrap.log_on_access:Request: del Target().ok", logged[0]) 194 | self.assertRegex( 195 | logged[1], 196 | r"DEBUG:logwrap\.log_on_access:Failed after (?:\d+\.\d{3})s: del Target\(\)\.ok", 197 | ) 198 | self.assertEqual("Traceback (most recent call last):", logged[2]) 199 | 200 | def test_05_negative_properties(self): 201 | # noinspection PyMissingOrEmptyDocstring 202 | class Target: 203 | def __init__(tself, val=VALUE): 204 | tself.val = val 205 | 206 | def __repr__(tself): 207 | return f"{tself.__class__.__name__}(val={tself.val})" 208 | 209 | @logwrap.LogOnAccess 210 | def ok(tself): 211 | raise AttributeError() 212 | 213 | ok.exc_level = logging.ERROR 214 | ok.log_traceback = False 215 | ok.log_object_repr = False 216 | ok.override_name = "override" 217 | 218 | target = Target() 219 | 220 | with self.assertRaises(AttributeError): 221 | self.assertIsNone(target.ok) 222 | 223 | logged = self.stream.getvalue().splitlines() 224 | self.assertEqual( 225 | f"DEBUG:logwrap.log_on_access:Request: .override", 226 | logged[0], 227 | ) 228 | self.assertRegex( 229 | logged[1], 230 | rf"ERROR:logwrap\.log_on_access:Failed after (?:\d+\.\d{{3}})s: \.override", 231 | ) 232 | 233 | self.assertEqual(len(logged), 2) 234 | 235 | def test_06_negative_no_log(self): 236 | # noinspection PyMissingOrEmptyDocstring 237 | class Target: 238 | def __init__(tself, val=VALUE): 239 | tself.val = val 240 | 241 | def __repr__(tself): 242 | return f"{tself.__class__.__name__}(val={tself.val})" 243 | 244 | @logwrap.LogOnAccess 245 | def ok(tself): 246 | raise AttributeError() 247 | 248 | ok.log_failure = False 249 | ok.log_before = False 250 | 251 | target = Target() 252 | 253 | with self.assertRaises(AttributeError): 254 | self.assertIsNone(target.ok) 255 | 256 | self.assertEqual(self.stream.getvalue(), "") 257 | 258 | def test_07_property_mimic(self): 259 | # noinspection PyMissingOrEmptyDocstring 260 | class Target: 261 | def __repr__(tself): 262 | return f"{tself.__class__.__name__}()" 263 | 264 | empty = logwrap.LogOnAccess(doc="empty_property") 265 | 266 | target = Target() 267 | 268 | with self.assertRaises(AttributeError): 269 | self.assertIsNone(target.empty) 270 | 271 | with self.assertRaises(AttributeError): 272 | target.empty = None 273 | 274 | with self.assertRaises(AttributeError): 275 | del target.empty 276 | 277 | self.assertEqual(self.stream.getvalue(), "") 278 | 279 | def test_08_logger(self): 280 | v_on_init_set = "on_init_set" 281 | v_on_init_name = "on_init_name" 282 | v_prop_set = "prop_set" 283 | v_prop_name = "prop_name" 284 | 285 | # noinspection PyMissingOrEmptyDocstring 286 | class Target: 287 | on_init_set = logwrap.LogOnAccess(logger=logging.getLogger(v_on_init_set), fget=lambda self: v_on_init_set) 288 | on_init_name = logwrap.LogOnAccess(logger=v_on_init_name, fget=lambda self: v_on_init_name) 289 | 290 | @logwrap.LogOnAccess 291 | def prop_set(self): 292 | return v_prop_set 293 | 294 | prop_set.logger = logging.getLogger(v_prop_set) 295 | 296 | @logwrap.LogOnAccess 297 | def prop_name(self): 298 | return v_prop_name 299 | 300 | prop_name.logger = v_prop_name 301 | 302 | def __repr__(tself): 303 | return f"{tself.__class__.__name__}()" 304 | 305 | target = Target() 306 | 307 | getattr(target, "on_init_set") # noqa: B009 308 | logged = self.stream.getvalue().splitlines() 309 | self.assertEqual("DEBUG:on_init_set:Request: Target().on_init_set", logged[0]) 310 | self.assertRegex( 311 | logged[1], 312 | rf"DEBUG:on_init_set:Done at (?:\d+\.\d{{3}})s: " 313 | rf"Target\(\)\.on_init_set -> {logwrap.pretty_repr(v_on_init_set)}", 314 | ) 315 | 316 | self.stream.seek(0) 317 | self.stream.truncate() 318 | 319 | getattr(target, "on_init_name") # noqa: B009 320 | logged = self.stream.getvalue().splitlines() 321 | self.assertEqual("DEBUG:on_init_name:Request: Target().on_init_name", logged[0]) 322 | self.assertRegex( 323 | logged[1], 324 | rf"DEBUG:on_init_name:Done at (?:\d+\.\d{{3}})s: " 325 | rf"Target\(\)\.on_init_name -> {logwrap.pretty_repr(v_on_init_name)}", 326 | ) 327 | 328 | self.stream.seek(0) 329 | self.stream.truncate() 330 | 331 | getattr(target, "prop_set") # noqa: B009 332 | logged = self.stream.getvalue().splitlines() 333 | self.assertEqual("DEBUG:prop_set:Request: Target().prop_set", logged[0]) 334 | self.assertRegex( 335 | logged[1], 336 | rf"DEBUG:prop_set:Done at (?:\d+\.\d{{3}})s: " 337 | rf"Target\(\)\.prop_set -> {logwrap.pretty_repr(v_prop_set)}", 338 | ) 339 | 340 | self.stream.seek(0) 341 | self.stream.truncate() 342 | 343 | getattr(target, "prop_name") # noqa: B009 344 | logged = self.stream.getvalue().splitlines() 345 | self.assertEqual("DEBUG:prop_name:Request: Target().prop_name", logged[0]) 346 | self.assertRegex( 347 | logged[1], 348 | rf"DEBUG:prop_name:Done at (?:\d+\.\d{{3}})s: " 349 | rf"Target\(\)\.prop_name -> {logwrap.pretty_repr(v_prop_name)}", 350 | ) 351 | 352 | def test_09_logger_implemented(self): 353 | # noinspection PyMissingOrEmptyDocstring 354 | class Target: 355 | def __init__(tself, val=VALUE): 356 | tself.val = val 357 | tself.logger = logging.getLogger(tself.__class__.__name__) 358 | 359 | def __repr__(tself): 360 | return f"{tself.__class__.__name__}(val={tself.val})" 361 | 362 | @logwrap.LogOnAccess 363 | def ok(tself): 364 | return tself.val 365 | 366 | @ok.setter 367 | def ok(tself, val): 368 | tself.val = val 369 | 370 | @ok.deleter 371 | def ok(tself): 372 | tself.val = "" 373 | 374 | target = Target() 375 | self.assertEqual(target.ok, VALUE) 376 | logged = self.stream.getvalue().splitlines() 377 | self.assertEqual("DEBUG:Target:Request: Target(val=ok).ok", logged[0]) 378 | self.assertRegex( 379 | logged[1], 380 | rf"DEBUG:Target:Done at (?:\d+\.\d{{3}})s: Target\(val=ok\)\.ok -> {logwrap.pretty_repr(VALUE)}", 381 | ) 382 | 383 | self.stream.seek(0) 384 | self.stream.truncate() 385 | 386 | target.ok = VALUE.upper() 387 | logged = self.stream.getvalue().splitlines() 388 | self.assertEqual( 389 | f"DEBUG:Target:Request: Target(val=ok).ok = {logwrap.pretty_repr(VALUE.upper())}", 390 | logged[0], 391 | ) 392 | self.assertRegex( 393 | logged[1], 394 | rf"DEBUG:Target:Done at (?:\d+\.\d{{3}})s: " 395 | rf"Target\(val=ok\)\.ok = {logwrap.pretty_repr(VALUE.upper())}", 396 | ) 397 | 398 | self.assertEqual(target.ok, VALUE.upper()) 399 | 400 | self.stream.seek(0) 401 | self.stream.truncate() 402 | 403 | del target.ok 404 | logged = self.stream.getvalue().splitlines() 405 | self.assertEqual("DEBUG:Target:Request: del Target(val=OK).ok", logged[0]) 406 | self.assertRegex(logged[1], r"DEBUG:Target:Done at (?:\d+\.\d{3})s: del Target\(val=OK\)\.ok") 407 | 408 | def test_10_log_implemented(self): 409 | # noinspection PyMissingOrEmptyDocstring 410 | class Target: 411 | def __init__(tself, val=VALUE): 412 | tself.val = val 413 | tself.log = logging.getLogger(tself.__class__.__name__) 414 | 415 | def __repr__(tself): 416 | return f"{tself.__class__.__name__}(val={tself.val})" 417 | 418 | @logwrap.LogOnAccess 419 | def ok(tself): 420 | return tself.val 421 | 422 | @ok.setter 423 | def ok(tself, val): 424 | tself.val = val 425 | 426 | @ok.deleter 427 | def ok(tself): 428 | tself.val = "" 429 | 430 | target = Target() 431 | self.assertEqual(target.ok, VALUE) 432 | logged = self.stream.getvalue().splitlines() 433 | self.assertEqual("DEBUG:Target:Request: Target(val=ok).ok", logged[0]) 434 | self.assertRegex( 435 | logged[1], 436 | rf"DEBUG:Target:Done at (?:\d+\.\d{{3}})s: Target\(val=ok\)\.ok -> {logwrap.pretty_repr(VALUE)}", 437 | ) 438 | 439 | self.stream.seek(0) 440 | self.stream.truncate() 441 | 442 | target.ok = VALUE.upper() 443 | logged = self.stream.getvalue().splitlines() 444 | self.assertEqual( 445 | f"DEBUG:Target:Request: Target(val=ok).ok = {logwrap.pretty_repr(VALUE.upper())}", 446 | logged[0], 447 | ) 448 | self.assertRegex( 449 | logged[1], 450 | rf"DEBUG:Target:Done at (?:\d+\.\d{{3}})s: " 451 | rf"Target\(val=ok\)\.ok = {logwrap.pretty_repr(VALUE.upper())}", 452 | ) 453 | 454 | self.assertEqual(target.ok, VALUE.upper()) 455 | 456 | self.stream.seek(0) 457 | self.stream.truncate() 458 | 459 | del target.ok 460 | logged = self.stream.getvalue().splitlines() 461 | self.assertEqual("DEBUG:Target:Request: del Target(val=OK).ok", logged[0]) 462 | self.assertRegex(logged[1], r"DEBUG:Target:Done at (?:\d+\.\d{3})s: del Target\(val=OK\)\.ok") 463 | -------------------------------------------------------------------------------- /test/test_log_on_access_mod_log.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | """Tests for logwrap.LogOnAccess with logger pick-up from module/instance.""" 4 | 5 | import io 6 | import logging 7 | import unittest 8 | 9 | import logwrap 10 | 11 | VALUE = "ok" 12 | LOG = logging.getLogger("Target_mod") 13 | 14 | 15 | class TestLogOnAccess(unittest.TestCase): 16 | def setUp(self): 17 | """Preparation for tests.""" 18 | self.stream = io.StringIO() 19 | logging.getLogger().handlers.clear() 20 | logging.basicConfig(level=logging.DEBUG, stream=self.stream) 21 | 22 | def tearDown(self): 23 | """Revert modifications.""" 24 | logging.getLogger().handlers.clear() 25 | self.stream.close() 26 | 27 | def test_01_logger(self): 28 | # noinspection PyMissingOrEmptyDocstring 29 | class Target: 30 | def __init__(tself, val=VALUE): 31 | tself.val = val 32 | 33 | def __repr__(tself): 34 | return f"{tself.__class__.__name__}(val={tself.val})" 35 | 36 | @logwrap.LogOnAccess 37 | def ok(tself): 38 | return tself.val 39 | 40 | @ok.setter 41 | def ok(tself, val): 42 | tself.val = val 43 | 44 | @ok.deleter 45 | def ok(tself): 46 | tself.val = "" 47 | 48 | target = Target() 49 | self.assertEqual(target.ok, VALUE) 50 | logged = self.stream.getvalue().splitlines() 51 | self.assertEqual("DEBUG:Target_mod:Request: Target(val=ok).ok", logged[0]) 52 | self.assertRegex( 53 | logged[1], 54 | rf"DEBUG:Target_mod:Done at (?:\d+\.\d{{3}})s: Target\(val=ok\)\.ok -> {logwrap.pretty_repr(VALUE)}", 55 | ) 56 | -------------------------------------------------------------------------------- /test/test_log_wrap_logger.py: -------------------------------------------------------------------------------- 1 | """LogWrap tests with logger instance pick-up from module.""" 2 | 3 | import io 4 | import logging 5 | import unittest 6 | 7 | import logwrap 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring 13 | class TestLogWrapLoggerInTargetModule(unittest.TestCase): 14 | def setUp(self): 15 | """Preparation for tests. 16 | 17 | Due to no possibility of proper mock patch of function defaults, modify directly. 18 | """ 19 | self.logger = LOGGER 20 | self.logger.setLevel(logging.DEBUG) 21 | 22 | self.stream = io.StringIO() 23 | 24 | self.logger.handlers.clear() 25 | handler = logging.StreamHandler(self.stream) 26 | handler.setFormatter(logging.Formatter(fmt="%(levelname)s>%(message)s")) 27 | self.logger.addHandler(handler) 28 | 29 | def tearDown(self): 30 | """Revert modifications.""" 31 | self.logger.handlers.clear() 32 | 33 | def test_001_simple(self): 34 | @logwrap.logwrap 35 | def func(): 36 | return "No args" 37 | 38 | result = func() 39 | self.assertEqual(result, "No args") 40 | 41 | self.assertEqual( 42 | f"DEBUG>Calling: \nfunc()\nDEBUG>Done: 'func' with result:\n{logwrap.pretty_repr(result)}\n", 43 | self.stream.getvalue(), 44 | ) 45 | 46 | def test_002_logger_no_prefetch(self): 47 | @logwrap.logwrap() 48 | def func(): 49 | return "No args" 50 | 51 | result = func() 52 | self.assertEqual(result, "No args") 53 | 54 | self.assertEqual( 55 | f"DEBUG>Calling: \nfunc()\nDEBUG>Done: 'func' with result:\n{logwrap.pretty_repr(result)}\n", 56 | self.stream.getvalue(), 57 | ) 58 | -------------------------------------------------------------------------------- /test/test_log_wrap_py3.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 - 2021 Alexey Stepanov aka penguinolog 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | # pylint: disable=unused-argument 16 | 17 | """Python 3 specific tests.""" 18 | 19 | from __future__ import annotations 20 | 21 | import asyncio 22 | import io 23 | import logging 24 | import unittest 25 | from unittest import mock 26 | 27 | import logwrap 28 | 29 | 30 | # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring 31 | @unittest.skipIf(not hasattr(asyncio, "coroutine"), "Python 3.11 do not have `asyncio.coroutine`") 32 | class TestLogWrapAsync(unittest.TestCase): 33 | @classmethod 34 | def setUpClass(cls): 35 | """Global preparation for tests (run once per class).""" 36 | cls.loop = asyncio.get_event_loop_policy().get_event_loop() 37 | 38 | def setUp(self): 39 | """Preparation for tests. 40 | 41 | Due to no possibility of proper mock patch of function defaults, modify directly. 42 | """ 43 | self.logger = logging.getLogger("logwrap") 44 | self.logger.setLevel(logging.DEBUG) 45 | 46 | self.stream = io.StringIO() 47 | 48 | self.logger.handlers.clear() 49 | handler = logging.StreamHandler(self.stream) 50 | handler.setFormatter(logging.Formatter(fmt="%(levelname)s>%(message)s")) 51 | self.logger.addHandler(handler) 52 | 53 | def tearDown(self): 54 | """Revert modifications.""" 55 | self.logger.handlers.clear() 56 | 57 | def test_coroutine_async(self): 58 | @logwrap.logwrap 59 | @asyncio.coroutine 60 | def func(): 61 | pass 62 | 63 | self.loop.run_until_complete(func()) 64 | self.assertEqual( 65 | "DEBUG>Awaiting: \nfunc()\nDEBUG>Done: 'func' with result:\nNone\n", 66 | self.stream.getvalue(), 67 | ) 68 | 69 | def test_coroutine_async_as_argumented(self): 70 | new_logger = mock.Mock(spec=logging.Logger, name="logger") 71 | log = mock.Mock(name="log") 72 | new_logger.attach_mock(log, "log") 73 | 74 | @logwrap.logwrap(log=new_logger) 75 | @asyncio.coroutine 76 | def func(): 77 | pass 78 | 79 | self.loop.run_until_complete(func()) 80 | 81 | self.assertEqual( 82 | [ 83 | mock.call.log(level=logging.DEBUG, msg="Awaiting: \nfunc()"), 84 | mock.call.log(level=logging.DEBUG, msg="Done: 'func' with result:\nNone"), 85 | ], 86 | log.mock_calls, 87 | ) 88 | 89 | def test_coroutine_fail(self): 90 | @logwrap.logwrap 91 | @asyncio.coroutine 92 | def func(): 93 | raise Exception("Expected") 94 | 95 | with self.assertRaises(Exception): # noqa: B017 96 | self.loop.run_until_complete(func()) 97 | 98 | self.assertEqual( 99 | "DEBUG>Awaiting: \nfunc()\nERROR>Failed: \nfunc()\nTraceback (most recent call last):", 100 | "\n".join(self.stream.getvalue().split("\n")[:5]), 101 | ) 102 | 103 | def test_exceptions_blacklist(self): 104 | new_logger = mock.Mock(spec=logging.Logger, name="logger") 105 | log = mock.Mock(name="log") 106 | new_logger.attach_mock(log, "log") 107 | 108 | @logwrap.logwrap(log=new_logger, blacklisted_exceptions=[TypeError]) 109 | @asyncio.coroutine 110 | def func(): 111 | raise TypeError("Blacklisted") 112 | 113 | with self.assertRaises(TypeError): 114 | self.loop.run_until_complete(func()) 115 | 116 | # While we're not expanding result coroutine object from namespace, 117 | # do not check execution result 118 | 119 | self.assertEqual( 120 | [ 121 | mock.call(level=logging.DEBUG, msg="Awaiting: \nfunc()"), 122 | mock.call( 123 | exc_info=False, 124 | level=40, 125 | msg=f"Failed: \nfunc()\n{TypeError.__name__}", 126 | ), 127 | ], 128 | log.mock_calls, 129 | ) 130 | 131 | 132 | # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring 133 | class TestAnnotated(unittest.TestCase): 134 | def setUp(self): 135 | """Preparation for tests. 136 | 137 | Due to no possibility of proper mock patch of function defaults, modify directly. 138 | """ 139 | self.logger = logging.getLogger("logwrap") 140 | self.logger.setLevel(logging.DEBUG) 141 | 142 | self.stream = io.StringIO() 143 | 144 | self.logger.handlers.clear() 145 | handler = logging.StreamHandler(self.stream) 146 | handler.setFormatter(logging.Formatter(fmt="%(levelname)s>%(message)s")) 147 | self.logger.addHandler(handler) 148 | 149 | def tearDown(self): 150 | """Revert modifications.""" 151 | self.logger.handlers.clear() 152 | 153 | def test_01_annotation_args(self): 154 | @logwrap.logwrap 155 | def func(arg: int | None = None): 156 | pass 157 | 158 | func() 159 | self.assertEqual( 160 | "DEBUG>Calling: \n" 161 | "func(\n" 162 | " # POSITIONAL_OR_KEYWORD:\n" 163 | " arg=None, # type: int | None\n" 164 | ")\n" 165 | "DEBUG>Done: 'func' with result:\n" 166 | "None\n", 167 | self.stream.getvalue(), 168 | ) 169 | 170 | def test_02_annotation_args(self): 171 | @logwrap.logwrap 172 | def func(arg: int = 0): 173 | pass 174 | 175 | func() 176 | self.assertEqual( 177 | "DEBUG>Calling: \n" 178 | "func(\n" 179 | " # POSITIONAL_OR_KEYWORD:\n" 180 | " arg=0, # type: int\n" 181 | ")\n" 182 | "DEBUG>Done: 'func' with result:\n" 183 | "None\n", 184 | self.stream.getvalue(), 185 | ) 186 | -------------------------------------------------------------------------------- /test/test_log_wrap_py35.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 - 2021 Alexey Stepanov aka penguinolog 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | # pylint: disable=missing-docstring, unused-argument 16 | 17 | """Python 3 specific tests""" 18 | 19 | import asyncio 20 | import io 21 | import logging 22 | import unittest 23 | from unittest import mock 24 | 25 | import logwrap 26 | 27 | 28 | # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring 29 | class TestLogWrapAsync(unittest.TestCase): 30 | """async def differs from asyncio.coroutine.""" 31 | 32 | def setUp(self): 33 | """Preparation for tests. 34 | 35 | Due to no possibility of proper mock patch of function defaults, modify directly. 36 | """ 37 | self.logger = logging.getLogger("logwrap") 38 | self.logger.setLevel(logging.DEBUG) 39 | 40 | self.stream = io.StringIO() 41 | 42 | self.logger.handlers.clear() 43 | handler = logging.StreamHandler(self.stream) 44 | handler.setFormatter(logging.Formatter(fmt="%(levelname)s>%(message)s")) 45 | self.logger.addHandler(handler) 46 | 47 | def tearDown(self): 48 | """Revert modifications.""" 49 | self.logger.handlers.clear() 50 | 51 | def test_coroutine_async(self): 52 | @logwrap.logwrap 53 | async def func(): 54 | pass 55 | 56 | asyncio.run(func()) 57 | self.assertEqual( 58 | "DEBUG>Awaiting: \nfunc()\nDEBUG>Done: 'func' with result:\nNone\n", 59 | self.stream.getvalue(), 60 | ) 61 | 62 | def test_coroutine_async_as_argumented(self): 63 | new_logger = mock.Mock(spec=logging.Logger, name="logger") 64 | log = mock.Mock(name="log") 65 | new_logger.attach_mock(log, "log") 66 | 67 | @logwrap.logwrap(log=new_logger) 68 | async def func(): 69 | pass 70 | 71 | asyncio.run(func()) 72 | 73 | self.assertEqual( 74 | [ 75 | mock.call.log(level=logging.DEBUG, msg="Awaiting: \nfunc()"), 76 | mock.call.log(level=logging.DEBUG, msg="Done: 'func' with result:\nNone"), 77 | ], 78 | log.mock_calls, 79 | ) 80 | 81 | def test_coroutine_fail(self): 82 | @logwrap.logwrap 83 | async def func(): 84 | raise Exception("Expected") 85 | 86 | with self.assertRaises(Exception): # noqa: B017 87 | asyncio.run(func()) 88 | 89 | self.assertEqual( 90 | "DEBUG>Awaiting: \nfunc()\nERROR>Failed: \nfunc()\nTraceback (most recent call last):", 91 | "\n".join(self.stream.getvalue().split("\n")[:5]), 92 | ) 93 | 94 | def test_exceptions_blacklist(self): 95 | new_logger = mock.Mock(spec=logging.Logger, name="logger") 96 | log = mock.Mock(name="log") 97 | new_logger.attach_mock(log, "log") 98 | 99 | @logwrap.logwrap(log=new_logger, blacklisted_exceptions=[TypeError]) 100 | async def func(): 101 | raise TypeError("Blacklisted") 102 | 103 | with self.assertRaises(TypeError): 104 | asyncio.run(func()) 105 | 106 | # While we're not expanding result coroutine object from namespace, 107 | # do not check execution result 108 | 109 | self.assertEqual( 110 | [ 111 | mock.call(level=logging.DEBUG, msg="Awaiting: \nfunc()"), 112 | mock.call( 113 | exc_info=False, 114 | level=40, 115 | msg=f"Failed: \nfunc()\n{TypeError.__name__}", 116 | ), 117 | ], 118 | log.mock_calls, 119 | ) 120 | -------------------------------------------------------------------------------- /test/test_log_wrap_shared.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 - 2021 Alexey Stepanov aka penguinolog 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | # pylint: disable=missing-docstring 16 | 17 | """_repr_utils (internal helpers) specific tests.""" 18 | 19 | import unittest 20 | from inspect import signature 21 | 22 | from logwrap import log_wrap 23 | 24 | 25 | def example_function(arg1, arg2: int = 2, *args, arg3, arg4: int = 4, **kwargs) -> None: 26 | """Function to use as signature source.""" 27 | 28 | 29 | sig = signature(example_function) 30 | 31 | 32 | # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring 33 | class TestBind(unittest.TestCase): 34 | def test_001_positive(self): 35 | params = list(log_wrap.bind_args_kwargs(sig, 1, arg3=33)) 36 | arg_1_bound = params[0] 37 | self.assertEqual(arg_1_bound.name, "arg1") 38 | self.assertEqual(arg_1_bound.value, 1) 39 | self.assertEqual(arg_1_bound.default, arg_1_bound.empty) 40 | self.assertEqual(arg_1_bound.annotation, arg_1_bound.empty) 41 | self.assertEqual(arg_1_bound.kind, arg_1_bound.POSITIONAL_OR_KEYWORD) 42 | self.assertEqual(str(arg_1_bound), "arg1=1") 43 | 44 | arg_2_bound = params[1] 45 | self.assertEqual(arg_2_bound.name, "arg2") 46 | self.assertEqual(arg_2_bound.value, 2) 47 | self.assertEqual(arg_2_bound.default, 2) 48 | self.assertEqual(arg_2_bound.annotation, int) 49 | self.assertEqual(arg_2_bound.kind, arg_2_bound.POSITIONAL_OR_KEYWORD) 50 | self.assertEqual(str(arg_2_bound), "arg2: int=2 # 2") 51 | 52 | args_bound = params[2] 53 | self.assertEqual(args_bound.name, "args") 54 | self.assertEqual(args_bound.value, args_bound.empty) 55 | self.assertEqual(args_bound.default, args_bound.empty) 56 | self.assertEqual(args_bound.annotation, args_bound.empty) 57 | self.assertEqual(args_bound.kind, args_bound.VAR_POSITIONAL) 58 | self.assertEqual(str(args_bound), "*args=()") 59 | 60 | arg_3_bound = params[3] 61 | self.assertEqual(arg_3_bound.name, "arg3") 62 | self.assertEqual(arg_3_bound.value, 33) 63 | self.assertEqual(arg_3_bound.default, arg_3_bound.empty) 64 | self.assertEqual(arg_3_bound.annotation, arg_3_bound.empty) 65 | self.assertEqual(arg_3_bound.kind, arg_3_bound.KEYWORD_ONLY) 66 | self.assertEqual(str(arg_3_bound), "arg3=33") 67 | 68 | arg_4_bound = params[4] 69 | self.assertEqual(arg_4_bound.name, "arg4") 70 | self.assertEqual(arg_4_bound.value, 4) 71 | self.assertEqual(arg_4_bound.default, 4) 72 | self.assertEqual(arg_4_bound.annotation, int) 73 | self.assertEqual(arg_4_bound.kind, arg_4_bound.KEYWORD_ONLY) 74 | self.assertEqual(str(arg_4_bound), "arg4: int=4 # 4") 75 | 76 | kwargs_bound = params[5] 77 | self.assertEqual(kwargs_bound.name, "kwargs") 78 | self.assertEqual(kwargs_bound.value, kwargs_bound.empty) 79 | self.assertEqual(kwargs_bound.default, kwargs_bound.empty) 80 | self.assertEqual(kwargs_bound.annotation, kwargs_bound.empty) 81 | self.assertEqual(kwargs_bound.kind, kwargs_bound.VAR_KEYWORD) 82 | self.assertEqual(str(kwargs_bound), "**kwargs={}") 83 | 84 | def test_002_args_kwargs(self): 85 | params = list(log_wrap.bind_args_kwargs(sig, 1, 2, 3, arg3=30, arg4=40, arg5=50)) 86 | 87 | args_bound = params[2] 88 | self.assertEqual(args_bound.name, "args") 89 | self.assertEqual(args_bound.value, (3,)) 90 | self.assertEqual(args_bound.default, args_bound.empty) 91 | self.assertEqual(args_bound.annotation, args_bound.empty) 92 | self.assertEqual(args_bound.kind, args_bound.VAR_POSITIONAL) 93 | self.assertEqual(str(args_bound), "*args=(3,)") 94 | 95 | kwargs_bound = params[5] 96 | self.assertEqual(kwargs_bound.name, "kwargs") 97 | self.assertEqual(kwargs_bound.value, {"arg5": 50}) 98 | self.assertEqual(kwargs_bound.default, kwargs_bound.empty) 99 | self.assertEqual(kwargs_bound.annotation, kwargs_bound.empty) 100 | self.assertEqual(kwargs_bound.kind, kwargs_bound.VAR_KEYWORD) 101 | self.assertEqual(str(kwargs_bound), "**kwargs={'arg5': 50}") 102 | 103 | def test_003_no_value(self): 104 | params = list(log_wrap.bind_args_kwargs(sig, 1, arg3=33)) 105 | arg_1_bound = params[0] 106 | arg1_parameter = arg_1_bound 107 | with self.assertRaises(ValueError): 108 | log_wrap.BoundParameter(arg1_parameter, arg1_parameter.empty) 109 | 110 | def test_004_annotations(self): 111 | def func(arg1, arg2: int, arg3: int = 3): 112 | pass 113 | 114 | sig = signature(func) 115 | params = list(log_wrap.bind_args_kwargs(sig, 1, 2, 4)) 116 | 117 | arg_1_bound = params[0] 118 | self.assertEqual(arg_1_bound.name, "arg1") 119 | self.assertEqual(arg_1_bound.value, 1) 120 | self.assertEqual(arg_1_bound.default, arg_1_bound.empty) 121 | self.assertEqual(arg_1_bound.annotation, arg_1_bound.empty) 122 | self.assertEqual(arg_1_bound.kind, arg_1_bound.POSITIONAL_OR_KEYWORD) 123 | self.assertEqual(str(arg_1_bound), "arg1=1") 124 | 125 | arg_2_bound = params[1] 126 | self.assertEqual(arg_2_bound.name, "arg2") 127 | self.assertEqual(arg_2_bound.value, 2) 128 | self.assertEqual(arg_2_bound.default, arg_2_bound.empty) 129 | self.assertEqual(arg_2_bound.annotation, int) 130 | self.assertEqual(arg_2_bound.kind, arg_2_bound.POSITIONAL_OR_KEYWORD) 131 | self.assertEqual(str(arg_2_bound), "arg2: int=2") 132 | 133 | arg_3_bound = params[2] 134 | self.assertEqual(arg_3_bound.name, "arg3") 135 | self.assertEqual(arg_3_bound.value, 4) 136 | self.assertEqual(arg_3_bound.default, 3) 137 | self.assertEqual(arg_3_bound.annotation, int) 138 | self.assertEqual(arg_3_bound.kind, arg_3_bound.POSITIONAL_OR_KEYWORD) 139 | self.assertEqual(str(arg_3_bound), "arg3: int=4 # 3") 140 | -------------------------------------------------------------------------------- /test/test_pretty_str.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 - 2021 Alexey Stepanov aka penguinolog 2 | 3 | # Copyright 2016 Mirantis, Inc. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | # pylint: disable=missing-docstring 18 | 19 | """pretty_str specific tests""" 20 | 21 | import unittest 22 | 23 | import logwrap 24 | 25 | 26 | # noinspection PyUnusedLocal 27 | class TestPrettyStr(unittest.TestCase): 28 | def test_simple(self): 29 | self.assertEqual(logwrap.pretty_str(True), str(True)) 30 | 31 | def test_text(self): 32 | self.assertEqual(logwrap.pretty_str("Unicode text"), "Unicode text") 33 | self.assertEqual(logwrap.pretty_str(b"bytes text\x01"), "bytes text\x01") 34 | 35 | def test_iterable(self): 36 | self.assertEqual( 37 | "[{nl:<5}1,{nl:<5}2,{nl:<5}3,\n]".format(nl="\n"), 38 | logwrap.pretty_str([1, 2, 3]), 39 | ) 40 | self.assertEqual( 41 | "({nl:<5}1,{nl:<5}2,{nl:<5}3,\n)".format(nl="\n"), 42 | logwrap.pretty_str((1, 2, 3)), 43 | ) 44 | res = logwrap.pretty_str({1, 2, 3}) 45 | self.assertTrue(res.startswith("{") and res.endswith("\n}")) 46 | res = logwrap.pretty_str(frozenset({1, 2, 3})) 47 | self.assertTrue(res.startswith("{") and res.endswith("\n}")) 48 | 49 | def test_simple_set(self): 50 | self.assertEqual(logwrap.pretty_str(set()), "set()") 51 | 52 | def test_dict(self): 53 | self.assertEqual( 54 | "{\n 1 : 1,\n 2 : 2,\n 33: 33,\n}", 55 | logwrap.pretty_str({1: 1, 2: 2, 33: 33}), 56 | ) 57 | 58 | def test_magic_override(self): 59 | # noinspection PyMissingOrEmptyDocstring 60 | class Tst: 61 | def __str__(self): 62 | return "Test" 63 | 64 | # noinspection PyMethodMayBeStatic 65 | def __pretty_str__(self, parser, indent, no_indent_start): 66 | return parser.process_element("Test Class", indent=indent, no_indent_start=no_indent_start) 67 | 68 | result = logwrap.pretty_str(Tst()) 69 | self.assertNotEqual(result, "Test") 70 | self.assertEqual(result, "Test Class") # .format(id(Tst)) 71 | -------------------------------------------------------------------------------- /test/test_repr_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 - 2021 Alexey Stepanov aka penguinolog 2 | 3 | # Copyright 2016 Mirantis, Inc. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | # pylint: disable=missing-docstring 18 | 19 | """_repr_utils (internal helpers) specific tests.""" 20 | 21 | from __future__ import annotations 22 | 23 | import argparse 24 | import collections 25 | import dataclasses 26 | import typing 27 | import unittest 28 | 29 | import logwrap 30 | 31 | if typing.TYPE_CHECKING: 32 | from collections.abc import Iterable 33 | 34 | from rich.repr import Result as RichReprResult 35 | 36 | 37 | # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring 38 | class TestPrettyRepr(unittest.TestCase): 39 | def test_001_simple(self): 40 | self.assertEqual(logwrap.pretty_repr(True), repr(True)) 41 | 42 | def test_002_text(self): 43 | txt = "Unicode text" 44 | b_txt = b"bytes text\x01" 45 | self.assertEqual(repr(txt), logwrap.pretty_repr(txt)) 46 | self.assertEqual(repr(b_txt), logwrap.pretty_repr(b_txt)) 47 | 48 | def test_003_iterable(self): 49 | self.assertEqual( 50 | "[{nl:<5}1,{nl:<5}2,{nl:<5}3,\n]".format(nl="\n"), 51 | logwrap.pretty_repr([1, 2, 3]), 52 | ) 53 | self.assertEqual( 54 | "({nl:<5}1,{nl:<5}2,{nl:<5}3,\n)".format(nl="\n"), 55 | logwrap.pretty_repr((1, 2, 3)), 56 | ) 57 | res = logwrap.pretty_repr({1, 2, 3}) 58 | self.assertTrue(res.startswith("{") and res.endswith("\n}")) 59 | res = logwrap.pretty_repr(frozenset({1, 2, 3})) 60 | self.assertTrue(res.startswith("frozenset({") and res.endswith("\n})")) 61 | 62 | def test_004_dict(self): 63 | self.assertEqual( 64 | "{\n 1 : 1,\n 2 : 2,\n 33: 33,\n}", 65 | logwrap.pretty_repr({1: 1, 2: 2, 33: 33}), 66 | ) 67 | 68 | def test_005_nested_obj(self): 69 | test_obj = [ 70 | {1: 2}, 71 | {3: {4}}, 72 | [5, 6, 7], 73 | (8, 9, 10), 74 | {}, 75 | [], 76 | (), 77 | set(), 78 | ] 79 | exp_repr = ( 80 | "[\n" 81 | " {\n" 82 | " 1: 2,\n" 83 | " },\n" 84 | " {\n" 85 | " 3: {\n" 86 | " 4,\n" 87 | " },\n" 88 | " },\n" 89 | " [\n" 90 | " 5,\n" 91 | " 6,\n" 92 | " 7,\n" 93 | " ],\n" 94 | " (\n" 95 | " 8,\n" 96 | " 9,\n" 97 | " 10,\n" 98 | " ),\n" 99 | " {},\n" 100 | " [],\n" 101 | " (),\n" 102 | " set(),\n" 103 | "]" 104 | ) 105 | self.assertEqual(exp_repr, logwrap.pretty_repr(test_obj)) 106 | 107 | def test_006_callable_simple(self): 108 | def empty_func(): 109 | pass 110 | 111 | self.assertEqual( 112 | f"{'':<{0}}" 113 | f"<{empty_func.__class__.__name__} {empty_func.__module__}.{empty_func.__name__} with interface ({''})>", 114 | logwrap.pretty_repr(empty_func), 115 | ) 116 | 117 | def test_007_callable_with_args(self): 118 | def full_func(arg, darg=1, *positional, **named): 119 | pass 120 | 121 | args = "\n arg,\n darg=1,\n *positional,\n **named,\n" 122 | 123 | self.assertEqual( 124 | f"{'':<{0}}" 125 | f"<{full_func.__class__.__name__} {full_func.__module__}.{full_func.__name__} with interface ({args})>", 126 | logwrap.pretty_repr(full_func), 127 | ) 128 | 129 | def test_008_callable_class_elements(self): 130 | # noinspection PyMissingOrEmptyDocstring 131 | class TstClass: 132 | def tst_method(self, arg, darg=1, *positional, **named): 133 | pass 134 | 135 | @classmethod 136 | def tst_classmethod(cls, arg, darg=1, *positional, **named): 137 | pass 138 | 139 | tst_instance = TstClass() 140 | 141 | # fmt: off 142 | c_m_args = ( 143 | "\n" 144 | " self,\n" 145 | " arg,\n" 146 | " darg=1,\n" 147 | " *positional,\n" 148 | " **named,\n" 149 | ) 150 | 151 | cm_args = ( 152 | "\n" 153 | f" cls={TstClass!r},\n" 154 | " arg,\n" 155 | " darg=1,\n" 156 | " *positional,\n" 157 | " **named,\n" 158 | ) 159 | # fmt: on 160 | 161 | i_m_args = f"\n self={tst_instance!r},\n arg,\n darg=1,\n *positional,\n **named,\n" 162 | 163 | for callable_obj, args in ( 164 | (TstClass.tst_method, c_m_args), 165 | (TstClass.tst_classmethod, cm_args), 166 | (tst_instance.tst_method, i_m_args), 167 | (tst_instance.tst_classmethod, cm_args), 168 | ): 169 | self.assertEqual( 170 | f"{'':<{0}}" 171 | f"<{callable_obj.__class__.__name__} {callable_obj.__module__}.{callable_obj.__name__} " 172 | f"with interface ({args})>", 173 | logwrap.pretty_repr(callable_obj), 174 | ) 175 | 176 | def test_009_indent(self): 177 | obj = [[[[[[[[[[123]]]]]]]]]] 178 | self.assertEqual( 179 | "[\n" 180 | " [\n" 181 | " [\n" 182 | " [\n" 183 | " [\n" 184 | " [\n" 185 | " [\n" 186 | " [\n" 187 | " [\n" 188 | " [\n" 189 | " 123,\n" 190 | " ],\n" 191 | " ],\n" 192 | " ],\n" 193 | " ],\n" 194 | " ],\n" 195 | " ],\n" 196 | " ],\n" 197 | " ],\n" 198 | " ],\n" 199 | "]", 200 | logwrap.pretty_repr(obj, max_indent=40), 201 | ) 202 | self.assertEqual( 203 | "[\n [\n [\n [[[[[[[123]]]]]]],\n ],\n ],\n]", 204 | logwrap.pretty_repr(obj, max_indent=10), 205 | ) 206 | 207 | def test_010_magic_override(self): 208 | # noinspection PyMissingOrEmptyDocstring 209 | class Tst: 210 | def __repr__(self): 211 | return "Test" 212 | 213 | def __pretty_repr__(self, parser, indent, no_indent_start): 214 | return parser.process_element( 215 | f"", 216 | indent=indent, 217 | no_indent_start=no_indent_start, 218 | ) 219 | 220 | result = logwrap.pretty_repr(Tst()) 221 | self.assertNotEqual(result, "Test") 222 | self.assertEqual(result, f"''") 223 | 224 | 225 | # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring 226 | class TestAnnotated(unittest.TestCase): 227 | def test_001_annotation_args(self): 228 | def func(a: int | None = None): 229 | pass 230 | 231 | args = "\n a: int | None = None,\n" 232 | 233 | self.assertEqual( 234 | f"{'':<{0}}<{func.__class__.__name__} {func.__module__}.{func.__name__} with interface ({args}){''}>", 235 | logwrap.pretty_repr(func), 236 | ) 237 | 238 | def test_002_annotation_return(self): 239 | def func() -> None: 240 | pass 241 | 242 | self.assertEqual( 243 | f"{'':<{0}}<{func.__class__.__name__} {func.__module__}.{func.__name__} with interface ({''}){' -> None'}>", 244 | logwrap.pretty_repr(func), 245 | ) 246 | 247 | def test_003_complex(self): 248 | def func(a: int | None = None) -> None: 249 | pass 250 | 251 | args = "\n a: int | None = None,\n" 252 | 253 | self.assertEqual( 254 | f"{'':<{0}}" 255 | f"<{func.__class__.__name__} {func.__module__}.{func.__name__} with interface ({args}){' -> None'}>", 256 | logwrap.pretty_repr(func), 257 | ) 258 | 259 | 260 | class TestContainers(unittest.TestCase): 261 | def test_001_argparse(self): 262 | parser = argparse.ArgumentParser(prog="Test") 263 | 264 | self.assertEqual( 265 | "argparse.ArgumentParser(\n" 266 | " prog='Test',\n" 267 | " usage=None,\n" 268 | " description=None,\n" 269 | " formatter_class=,\n" 270 | " conflict_handler='error',\n" 271 | " add_help=True,\n" 272 | ")", 273 | logwrap.pretty_repr(parser), 274 | ) 275 | 276 | def test_002_named_tuple_basic(self): 277 | NTTest = collections.namedtuple( # noqa: PYI024 # we need old one 278 | "NTTest", 279 | ("test_field_1", "test_field_2"), 280 | ) 281 | test_val = NTTest(1, 2) 282 | self.assertEqual( 283 | "test_repr_utils.NTTest(\n test_field_1=1,\n test_field_2=2,\n)", 284 | logwrap.pretty_repr(test_val), 285 | ) 286 | 287 | def test_003_typed(self): 288 | class NTTest(typing.NamedTuple): 289 | test_field_1: int 290 | test_field_2: int 291 | 292 | test_val = NTTest(1, 2) 293 | self.assertEqual( 294 | "test_repr_utils.NTTest(\n test_field_1=1, # type: int\n test_field_2=2, # type: int\n)", 295 | logwrap.pretty_repr(test_val), 296 | ) 297 | 298 | def test_004_dataclasses(self): 299 | @dataclasses.dataclass 300 | class TestDataClass: 301 | b: int = 0 302 | c: int = dataclasses.field(default=0, repr=False) 303 | d: tuple[str] = dataclasses.field(default=("d",)) 304 | 305 | test_dc = TestDataClass() 306 | 307 | self.assertEqual( 308 | "test_repr_utils.TestDataClass(\n" 309 | " b=0, # type: int\n" 310 | " d=(\n" 311 | " 'd',\n" 312 | " ), # type: tuple[str]\n" 313 | ")", 314 | logwrap.pretty_repr(test_dc), 315 | ) 316 | 317 | def test_005_deque(self): 318 | default_deque = collections.deque() 319 | default_deque.append("middle") 320 | default_deque.extend(("next", "last")) 321 | default_deque.extendleft(("second", "first")) 322 | 323 | self.assertEqual( 324 | "deque(\n" 325 | " (\n" 326 | " 'first',\n" 327 | " 'second',\n" 328 | " 'middle',\n" 329 | " 'next',\n" 330 | " 'last',\n" 331 | " ),\n" 332 | " maxlen=None,\n" 333 | ")", 334 | logwrap.pretty_repr(default_deque), 335 | ) 336 | 337 | def tests_006_union_ann(self): 338 | @dataclasses.dataclass 339 | class WithUnionAnn: 340 | a: int | None 341 | 342 | test_dc = WithUnionAnn(None) 343 | self.assertEqual( 344 | "test_repr_utils.WithUnionAnn(\n" 345 | " a=None, # type: int | None\n" 346 | ")", 347 | logwrap.pretty_repr(test_dc), 348 | ) 349 | 350 | 351 | class TestRich(unittest.TestCase): 352 | class Bird: 353 | def __init__( 354 | self, 355 | name: str, 356 | eats: Iterable[str] = (), 357 | fly: bool = True, 358 | ) -> None: 359 | self.name = name 360 | self.eats = list(eats) 361 | self.fly = fly 362 | 363 | def __rich_repr__(self) -> RichReprResult: 364 | yield self.name 365 | yield "eats", self.eats 366 | yield "fly", self.fly, True 367 | 368 | def test_001_skip_def(self): 369 | eats = ["fish", "chips", "ice cream", "sausage rolls"] 370 | val = self.Bird("gull", eats=eats) 371 | 372 | self.assertEqual( 373 | f"test_repr_utils.Bird(\n" 374 | f" 'gull',\n" 375 | f" eats={logwrap.pretty_repr(eats, indent=4, no_indent_start=True)},\n" 376 | f")", 377 | logwrap.pretty_repr(val), 378 | ) 379 | 380 | def test_002_ndef(self): 381 | eats = ["fish"] 382 | val = self.Bird("penguin", eats=eats, fly=False) 383 | 384 | self.assertEqual( 385 | f"test_repr_utils.Bird(\n" 386 | f" 'penguin',\n" 387 | f" eats={logwrap.pretty_repr(eats, indent=4, no_indent_start=True)},\n" 388 | f" fly=False,\n" 389 | f")", 390 | logwrap.pretty_repr(val), 391 | ) 392 | -------------------------------------------------------------------------------- /test/test_repr_utils_special.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2021 Alexey Stepanov aka penguinolog 2 | 3 | # Copyright 2016 Mirantis, Inc. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | # pylint: disable=missing-docstring 18 | 19 | """_repr_utils (internal helpers) specific tests.""" 20 | 21 | import typing 22 | import unittest 23 | 24 | import logwrap 25 | 26 | 27 | # noinspection PyUnusedLocal 28 | class TestPrettyRepr(unittest.TestCase): 29 | def test_001_dict_subclass(self): 30 | class MyDict(dict): 31 | """Dict subclass.""" 32 | 33 | val = MyDict(key="value") 34 | self.assertEqual("MyDict({\n 'key': 'value',\n})", logwrap.pretty_repr(val)) 35 | 36 | self.assertEqual("{\n key: value,\n}", logwrap.pretty_str(val)) 37 | 38 | def test_002_typing_specific_dict(self): 39 | class MyDict(dict[str, str]): 40 | """Dict subclass.""" 41 | 42 | val = MyDict(key="value") 43 | self.assertEqual("MyDict({\n 'key': 'value',\n})", logwrap.pretty_repr(val)) 44 | 45 | self.assertEqual("{\n key: value,\n}", logwrap.pretty_str(val)) 46 | 47 | def test_003_typing_specific_dict_repr_override(self): 48 | class MyDict(dict[str, str]): 49 | """Dict subclass.""" 50 | 51 | def __repr__(self) -> str: 52 | return f"{self.__class__.__name__}({tuple(zip(self.items()))})" 53 | 54 | def __str__(self) -> str: 55 | return str(dict(self)) 56 | 57 | val = MyDict(key="value") 58 | self.assertEqual("MyDict(((('key', 'value'),),))", logwrap.pretty_repr(val)) 59 | 60 | self.assertEqual("{'key': 'value'}", logwrap.pretty_str(val)) 61 | 62 | def test_004_typed_dict(self): 63 | # noinspection PyMissingOrEmptyDocstring 64 | class MyDict(typing.TypedDict): 65 | key: str 66 | 67 | val = MyDict(key="value") 68 | self.assertEqual("{\n 'key': 'value',\n}", logwrap.pretty_repr(val)) 69 | 70 | self.assertEqual("{\n key: value,\n}", logwrap.pretty_str(val)) 71 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | minversion = 3.15 8 | envlist = isort, black, ruff, pylint, mypy, refurb, pep257, py3{8,9,10,11,12}, readme, doc8, docs 9 | skip_missing_interpreters = True 10 | 11 | [testenv] 12 | recreate = True 13 | usedevelop = True 14 | passenv = 15 | http_proxy 16 | HTTP_PROXY 17 | https_proxy 18 | HTTPS_PROXY 19 | no_proxy 20 | NO_PROXY 21 | setev = PYTHONDONTWRITEBYTECODE=1 22 | deps = 23 | sphinx 24 | -r{toxinidir}/pytest_requirements.txt 25 | pytest-html 26 | 27 | commands = 28 | pip freeze 29 | py.test --self-contained-html --html=report.html 30 | 31 | [testenv:py3{8,9,10,11,12}] 32 | depends = pylint,mypy,pep8,ruff,refurb,pep257,bandit,black,isort 33 | 34 | [testenv:venv] 35 | commands = {posargs:} 36 | 37 | [testenv:ruff] 38 | skip_install = true 39 | depends = black,isort 40 | deps = ruff>=0.1.7 41 | commands = ruff check . 42 | 43 | [testenv:refurb] 44 | skip_install = true 45 | depends = black,isort 46 | deps = refurb 47 | commands = refurb logwrap 48 | 49 | [testenv:pep8] 50 | skip_install = true 51 | depends = black,isort 52 | deps = 53 | -r{toxinidir}/flake8_requirements.txt 54 | commands = flake8 logwrap 55 | 56 | [testenv:pep257] 57 | skip_install = true 58 | depends = black,isort 59 | deps = 60 | pydocstyle[toml] 61 | commands = pydocstyle -v logwrap 62 | 63 | [testenv:doc8] 64 | skip_install = true 65 | deps = 66 | doc8 67 | Pygments 68 | commands = doc8 README.rst doc/source 69 | 70 | [testenv:install] 71 | deps = 72 | commands = pip install ./ -vvv -U 73 | 74 | [testenv:pylint] 75 | depends = pep8,ruff,pep257,bandit 76 | deps = 77 | pylint>=3.0.0 78 | commands = 79 | pylint logwrap 80 | 81 | [testenv:docs] 82 | depends = doc8,readme 83 | deps = 84 | sphinx 85 | commands = sphinx-build doc/source/ doc/build 86 | 87 | [testenv:readme] 88 | skip_install = true 89 | deps = 90 | twine 91 | build 92 | commands = 93 | python -m build -s 94 | twine check {toxinidir}/dist/* 95 | 96 | [testenv:bandit] 97 | depends = black,isort 98 | deps = bandit 99 | commands = bandit -r logwrap 100 | 101 | [testenv:dep-graph] 102 | deps = 103 | . 104 | pipdeptree 105 | commands = pipdeptree 106 | 107 | [testenv:black] 108 | skip_install = true 109 | depends = isort 110 | deps = 111 | black 112 | regex 113 | commands = 114 | black logwrap 115 | 116 | [testenv:mypy] 117 | depends = pep8,ruff,pep257,bandit 118 | deps = 119 | mypy>=1.7.0 120 | lxml 121 | rich 122 | commands = 123 | mypy --strict --show-error-codes --xslt-html-report mypy_report -p logwrap 124 | 125 | [testenv:isort] 126 | skip_install = true 127 | deps = 128 | isort 129 | commands = 130 | isort logwrap 131 | 132 | [flake8] 133 | exclude = 134 | .venv, 135 | .git, 136 | .tox, 137 | dist, 138 | doc, 139 | *lib/python*, 140 | *egg, 141 | build, 142 | __init__.py, 143 | _version.py, 144 | docs 145 | ignore = 146 | E203, 147 | # whitespace before ':' 148 | W503, 149 | # line break before binary operator 150 | D401, 151 | # First line should be in imperative mood; try rephrasing 152 | D202, 153 | # No blank lines allowed after function docstring 154 | D203, 155 | # 1 blank line required before class docstring 156 | D213 157 | # Multi-line docstring summary should start at the second line 158 | show-pep8 = True 159 | show-source = True 160 | count = True 161 | max-line-length = 120 162 | --------------------------------------------------------------------------------