├── .coveragerc ├── .flake8 ├── .github └── workflows │ └── ci-tests.yml ├── .gitignore ├── CHANGES.rst ├── CONTRIBUTORS.txt ├── COPYRIGHT.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── api.rst ├── conf.py ├── glossary.rst └── index.rst ├── pyproject.toml ├── rtd.txt ├── setup.cfg ├── setup.py ├── src └── pyramid_exclog │ └── __init__.py ├── tests ├── __init__.py └── test_it.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | parallel = true 3 | source = 4 | pyramid_exclog 5 | 6 | [paths] 7 | source = 8 | src/pyramid_exclog 9 | */src/pyramid_exclog 10 | */site-packages/pyramid_exclog 11 | 12 | [report] 13 | show_missing = true 14 | precision = 2 15 | 16 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # Recommended flake8 settings while editing, we use Black for the final linting/say in how code is formatted 2 | # 3 | # pip install flake8 flake8-bugbear 4 | # 5 | # This will warn/error on things that black does not fix, on purpose. 6 | # 7 | # Run: 8 | # 9 | # tox -e run-flake8 10 | # 11 | # To have it automatically create and install the appropriate tools, and run 12 | # flake8 across the source code/tests 13 | 14 | [flake8] 15 | # max line length is set to 88 in black, here it is set to 80 and we enable bugbear's B950 warning, which is: 16 | # 17 | # B950: Line too long. This is a pragmatic equivalent of pycodestyle’s E501: it 18 | # considers “max-line-length” but only triggers when the value has been 19 | # exceeded by more than 10%. You will no longer be forced to reformat code due 20 | # to the closing parenthesis being one character too far to satisfy the linter. 21 | # At the same time, if you do significantly violate the line length, you will 22 | # receive a message that states what the actual limit is. This is inspired by 23 | # Raymond Hettinger’s “Beyond PEP 8” talk and highway patrol not stopping you 24 | # if you drive < 5mph too fast. Disable E501 to avoid duplicate warnings. 25 | max-line-length = 80 26 | max-complexity = 12 27 | select = E,F,W,C,B,B9 28 | ignore = 29 | # E123 closing bracket does not match indentation of opening bracket’s line 30 | E123 31 | # E203 whitespace before ‘:’ (Not PEP8 compliant, Python Black) 32 | E203 33 | # E501 line too long (82 > 79 characters) (replaced by B950 from flake8-bugbear, https://github.com/PyCQA/flake8-bugbear) 34 | E501 35 | # W503 line break before binary operator (Not PEP8 compliant, Python Black) 36 | W503 37 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | # Only on pushes to main or one of the release branches we build on push 5 | push: 6 | branches: 7 | - main 8 | - "[0-9].[0-9]+-branch" 9 | tags: 10 | # Build pull requests 11 | pull_request: 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | py: 18 | - "3.7" 19 | - "3.8" 20 | - "3.9" 21 | - "3.10" 22 | - "pypy-3.8" 23 | os: 24 | - "ubuntu-latest" 25 | - "windows-latest" 26 | - "macos-latest" 27 | architecture: 28 | - x64 29 | - x86 30 | 31 | exclude: 32 | # Linux and macOS don't have x86 python 33 | - os: "ubuntu-latest" 34 | architecture: x86 35 | - os: "macos-latest" 36 | architecture: x86 37 | 38 | name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v2 42 | - name: Setup python 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: ${{ matrix.py }} 46 | architecture: ${{ matrix.architecture }} 47 | - run: pip install tox 48 | - name: Running tox 49 | run: tox -e py 50 | test-py37-pyramid15: 51 | runs-on: ubuntu-latest 52 | name: "Python: 3.7-x64 on ubuntu-latest with Pyramid 1.5" 53 | steps: 54 | - uses: actions/checkout@v2 55 | - name: Setup python 56 | uses: actions/setup-python@v2 57 | with: 58 | python-version: "3.7" 59 | architecture: x64 60 | - run: pip install tox 61 | - name: Running tox 62 | run: tox -e py37-pyramid15 63 | test-py37-pyramid110: 64 | runs-on: ubuntu-latest 65 | name: "Python: 3.7-x64 on ubuntu-latest with Pyramid 1.10" 66 | steps: 67 | - uses: actions/checkout@v2 68 | - name: Setup python 69 | uses: actions/setup-python@v2 70 | with: 71 | python-version: "3.7" 72 | architecture: x64 73 | - run: pip install tox 74 | - name: Running tox 75 | run: tox -e py37-pyramid110 76 | coverage: 77 | runs-on: ubuntu-latest 78 | name: Validate coverage 79 | steps: 80 | - uses: actions/checkout@v2 81 | - name: Setup python 82 | uses: actions/setup-python@v2 83 | with: 84 | python-version: "3.10" 85 | architecture: x64 86 | 87 | - run: pip install tox 88 | - run: tox -e py310,coverage 89 | docs: 90 | runs-on: ubuntu-latest 91 | name: Build the documentation 92 | steps: 93 | - uses: actions/checkout@v2 94 | - name: Setup python 95 | uses: actions/setup-python@v2 96 | with: 97 | python-version: "3.10" 98 | architecture: x64 99 | - run: pip install tox 100 | - run: tox -e docs 101 | lint: 102 | runs-on: ubuntu-latest 103 | name: Lint the package 104 | steps: 105 | - uses: actions/checkout@v2 106 | - name: Setup python 107 | uses: actions/setup-python@v2 108 | with: 109 | python-version: "3.10" 110 | architecture: x64 111 | - run: pip install tox 112 | - run: tox -e lint 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info 3 | *.pyc 4 | *$py.class 5 | *.pt.py 6 | *.txt.py 7 | *~ 8 | .coverage 9 | .coverage.* 10 | build/ 11 | dist/ 12 | .tox/ 13 | coverage*.xml 14 | nosetests*.xml 15 | pyramid_exclog/coverage.xml 16 | env26/ 17 | env27/ 18 | env32/ 19 | coverage.xml 20 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Unreleased (2022-11-15) 2 | ----------------------- 3 | 4 | - Rename "master" to "main" 5 | 6 | 1.1 (2022-03-12) 7 | ---------------- 8 | 9 | - Drop support for Python 2.7, 3.5, and 3.6. 10 | 11 | - Support Python 3.7, 3.8, 3.9, 3.10. 12 | See https://github.com/Pylons/pyramid_exclog/pull/35 13 | 14 | - Add ``exclog.hide_cookies`` config option to mark certain 15 | cookie values as hidden from messages. 16 | See https://github.com/Pylons/pyramid_exclog/pull/39 17 | 18 | - Include the license file in the wheel. 19 | See https://github.com/Pylons/pyramid_exclog/pull/37 20 | 21 | - Refactor source repo, blackify, and remove tests from package. 22 | See https://github.com/Pylons/pyramid_exclog/pull/41 23 | 24 | 1.0 (2017-04-09) 25 | ---------------- 26 | 27 | - Drop support for Python 3.3. 28 | 29 | - Require Pyramid 1.5+. 30 | 31 | - Move the tween **over** the ``EXCVIEW`` such that it also handles 32 | exceptions caused by exception views. 33 | See https://github.com/Pylons/pyramid_exclog/pull/32 34 | 35 | 0.8 (2016-09-22) 36 | ---------------- 37 | 38 | - Drop support for Python 2.6 and 3.2. 39 | 40 | - Add explicit support for Python 3.4 and 3.5. 41 | 42 | - Handle IOError exception when accessing request parameters. 43 | 44 | - Fix UnicodeDecodeError on Python 2 when QUERY_STRING is a ``str`` 45 | containing non-ascii bytes. 46 | 47 | - Allways pass the logging module text rather than sometimes 48 | bytes and sometimes text. 49 | 50 | 0.7 (2013-06-28) 51 | ---------------- 52 | 53 | - Add explicit support for Python 3.3. 54 | 55 | - Do not error if the URL, query string or post data contains unexpected 56 | encodings. 57 | 58 | - Try to log an exception when logging fails: often the middleware is used 59 | just inside one which converts all errors into ServerErrors (500), hiding 60 | any exceptions triggered while logging. 61 | 62 | - Add ``unauthenticated_user()`` to the output when the ``extra_info`` key 63 | is set to True (PR #11). 64 | 65 | - Add a hook for constructing custom log messages (PR #15). 66 | 67 | - Changed testing regime to allow ``setup.py dev``. 68 | 69 | - We no longer test under Python 2.5 (although it's not explicitly broken 70 | under 2.5). 71 | 72 | 0.6 (2012-03-24) 73 | ---------------- 74 | 75 | - Add an ``exclog.extra_info`` setting to the exclog configuration. If it's 76 | true, send WSGI environment and params info in the log message. 77 | 78 | 0.5 (2011-09-27) 79 | ---------------- 80 | 81 | - Python 3.2 compatibility under Pyramid 1.3.X. 82 | 83 | 0.4 (2011-08-24) 84 | ----------------- 85 | 86 | - Docs-only changes. 87 | 88 | 0.3 (2011-08-21) 89 | ---------------- 90 | 91 | - Don't register an implicit tween factory with an alias (compat with future 92 | 1.2). 93 | 94 | 0.2 (2011-08-13) 95 | ---------------- 96 | 97 | - Improve documentation by providing examples of logging to file, email and 98 | by describing deltas to default Pyramid 1.2 logging config. 99 | 100 | - Use string value as factory to add_tween in includeme. 101 | 102 | 0.1 (2011-08-11) 103 | ---------------- 104 | 105 | - Initial release. 106 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Pylons Project Contributor Agreement 2 | ==================================== 3 | 4 | The submitter agrees by adding his or her name within the section below named 5 | "Contributors" and submitting the resulting modified document to the 6 | canonical shared repository location for this software project (whether 7 | directly, as a user with "direct commit access", or via a "pull request"), he 8 | or she is signing a contract electronically. The submitter becomes a 9 | Contributor after a) he or she signs this document by adding their name 10 | beneath the "Contributors" section below, and b) the resulting document is 11 | accepted into the canonical version control repository. 12 | 13 | Treatment of Account 14 | --------------------- 15 | 16 | Contributor will not allow anyone other than the Contributor to use his or 17 | her username or source repository login to submit code to a Pylons Project 18 | source repository. Should Contributor become aware of any such use, 19 | Contributor will immediately by notifying Agendaless Consulting. 20 | Notification must be performed by sending an email to 21 | webmaster@agendaless.com. Until such notice is received, Contributor will be 22 | presumed to have taken all actions made through Contributor's account. If the 23 | Contributor has direct commit access, Agendaless Consulting will have 24 | complete control and discretion over capabilities assigned to Contributor's 25 | account, and may disable Contributor's account for any reason at any time. 26 | 27 | Legal Effect of Contribution 28 | ---------------------------- 29 | 30 | Upon submitting a change or new work to a Pylons Project source Repository (a 31 | "Contribution"), you agree to assign, and hereby do assign, a one-half 32 | interest of all right, title and interest in and to copyright and other 33 | intellectual property rights with respect to your new and original portions 34 | of the Contribution to Agendaless Consulting. You and Agendaless Consulting 35 | each agree that the other shall be free to exercise any and all exclusive 36 | rights in and to the Contribution, without accounting to one another, 37 | including without limitation, the right to license the Contribution to others 38 | under the Repoze Public License. This agreement shall run with title to the 39 | Contribution. Agendaless Consulting does not convey to you any right, title 40 | or interest in or to the Program or such portions of the Contribution that 41 | were taken from the Program. Your transmission of a submission to the Pylons 42 | Project source Repository and marks of identification concerning the 43 | Contribution itself constitute your intent to contribute and your assignment 44 | of the work in accordance with the provisions of this Agreement. 45 | 46 | License Terms 47 | ------------- 48 | 49 | Code committed to the Pylons Project source repository (Committed Code) must 50 | be governed by the Repoze Public License (see LICENSE.txt, aka "the RPL") 51 | or another license acceptable to Agendaless Consulting. Until 52 | Agendaless Consulting declares in writing an acceptable license other than 53 | the RPL, only the RPL shall be used. A list of exceptions is detailed within 54 | the "Licensing Exceptions" section of this document, if one exists. 55 | 56 | Representations, Warranty, and Indemnification 57 | ---------------------------------------------- 58 | 59 | Contributor represents and warrants that the Committed Code does not violate 60 | the rights of any person or entity, and that the Contributor has legal 61 | authority to enter into this Agreement and legal authority over Contributed 62 | Code. Further, Contributor indemnifies Agendaless Consulting against 63 | violations. 64 | 65 | Cryptography 66 | ------------ 67 | 68 | Contributor understands that cryptographic code may be subject to government 69 | regulations with which Agendaless Consulting and/or entities using Committed 70 | Code must comply. Any code which contains any of the items listed below must 71 | not be checked-in until Agendaless Consulting staff has been notified and has 72 | approved such contribution in writing. 73 | 74 | - Cryptographic capabilities or features 75 | 76 | - Calls to cryptographic features 77 | 78 | - User interface elements which provide context relating to cryptography 79 | 80 | - Code which may, under casual inspection, appear to be cryptographic. 81 | 82 | Notices 83 | ------- 84 | 85 | Contributor confirms that any notices required will be included in any 86 | Committed Code. 87 | 88 | List of Contributors 89 | ==================== 90 | 91 | The below-signed are contributors to a code repository that is part of the 92 | project named "pyramid_exclog". Each below-signed contributor has read, 93 | understand and agrees to the terms above in the section within this document 94 | entitled "Pylons Project Contributor Agreement" as of the date beside his or 95 | her name. 96 | 97 | Contributors 98 | ------------ 99 | 100 | - Chris McDonough, 2011/08/11 101 | - Jason McKellar, 2012/01/09 102 | - Rob Miller, 2012/04/24 103 | - Bruk Habtu, 2012/11/23 104 | - Wyatt Baldwin, 2013/06/15 105 | - Brian Sutherland, 2013/06/19 106 | - Marco Falcioni, 2016/09/21 107 | - Jon Betts, 2021/04/19 108 | - Lynn Vaughan, 2022/02/22 109 | - Jonathan Vanasco, 2022/11/15 110 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Agendaless Consulting and Contributors. 2 | (http://www.agendaless.com), All Rights Reserved 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | A copyright notice accompanies this license document that identifies 2 | the copyright holders. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | 1. Redistributions in source code must retain the accompanying 9 | copyright notice, this list of conditions, and the following 10 | disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the accompanying 13 | copyright notice, this list of conditions, and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | 3. Names of the copyright holders must not be used to endorse or 18 | promote products derived from this software without prior 19 | written permission from the copyright holders. 20 | 21 | 4. If any files are modified, you must cause the modified files to 22 | carry prominent notices stating that you changed the files and 23 | the date of any change. 24 | 25 | Disclaimer 26 | 27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND 28 | ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 29 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 30 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 31 | HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 32 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 33 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 34 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 35 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 36 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 37 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 38 | SUCH DAMAGE. 39 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src/pyramid_tm 2 | graft tests 3 | graft docs 4 | graft .github 5 | 6 | include README.rst 7 | include CHANGES.rst 8 | include LICENSE.txt 9 | include CONTRIBUTORS.txt 10 | include COPYRIGHT.txt 11 | include CHANGES.rst 12 | 13 | include setup.cfg pyproject.toml 14 | include .coveragerc .flake8 15 | include tox.ini rtd.txt 16 | 17 | recursive-exclude * __pycache__ *.py[cod] 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``pyramid_exclog`` 2 | ================== 3 | 4 | A package which logs Pyramid application exception (error) information to a 5 | standard Python logger. This add-on is most useful when used in production 6 | applications, because the logger can be configured to log to a file, to UNIX 7 | syslog, to the Windows Event Log, or even to email. 8 | 9 | See the documentation at 10 | https://docs.pylonsproject.org/projects/pyramid-exclog/en/latest/ for more 11 | information. 12 | 13 | This package will only work with Pyramid 1.5 and better. 14 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | _themes/ 3 | .static 4 | -------------------------------------------------------------------------------- /docs/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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/hupper.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hupper.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/hupper" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/hupper" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _pyramid_exclog_api: 2 | 3 | :mod:`pyramid_exclog` API 4 | --------------------------- 5 | 6 | .. automodule:: pyramid_exclog 7 | 8 | .. autofunction:: includeme 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyramid_exclog documentation build configuration file 4 | # 5 | # This file is execfile()d with the current directory set to its containing 6 | # dir. 7 | # 8 | # The contents of this file are pickled, so don't put values in the 9 | # namespace that aren't pickleable (module imports are okay, they're 10 | # removed automatically). 11 | # 12 | # All configuration values have a default value; values that are commented 13 | # out serve to show the default value. 14 | 15 | # If your extensions are in another directory, add it here. If the 16 | # directory is relative to the documentation root, use os.path.abspath to 17 | # make it absolute, like shown here. 18 | #sys.path.append(os.path.abspath('some/directory')) 19 | 20 | import datetime 21 | import sys, os 22 | 23 | # General configuration 24 | # --------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [ 29 | 'sphinx.ext.autodoc', 30 | 'sphinx.ext.intersphinx', 31 | ] 32 | 33 | # Looks for pyramid's objects 34 | intersphinx_mapping = { 35 | 'pyramid': 36 | ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None)} 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['.templates'] 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The main toctree document. 45 | master_doc = 'index' 46 | 47 | # General substitutions. 48 | project = 'pyramid_exclog' 49 | thisyear = datetime.datetime.now().year 50 | copyright = '2011-%s, Agendaless Consulting ' % thisyear 51 | 52 | # The default replacements for |version| and |release|, also used in various 53 | # other places throughout the built documents. 54 | # 55 | # The short X.Y version. 56 | import pkg_resources 57 | version = pkg_resources.get_distribution(project).version 58 | # The full version, including alpha/beta/rc tags. 59 | release = version 60 | 61 | # There are two options for replacing |today|: either, you set today to 62 | # some non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | today_fmt = '%B %d, %Y' 66 | 67 | # List of documents that shouldn't be included in the build. 68 | #unused_docs = [] 69 | 70 | # List of directories, relative to source directories, that shouldn't be 71 | # searched for source files. 72 | #exclude_dirs = [] 73 | 74 | # The reST default role (used for this markup: `text`) to use for all 75 | # documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | 93 | # Options for HTML output 94 | # ----------------------- 95 | 96 | # Add and use Pylons theme 97 | import pylons_sphinx_themes 98 | html_theme_path = pylons_sphinx_themes.get_html_themes_path() 99 | html_theme = 'pyramid' 100 | html_theme_options = dict(github_url='http://github.com/Pylons/pyramid_exclog') 101 | 102 | # The style sheet to use for HTML and HTML Help pages. A file of that name 103 | # must exist either in Sphinx' static/ path, or in one of the custom paths 104 | # given in html_static_path. 105 | # html_style = 'repoze.css' 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as 112 | # html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (within the static path) to place at the top of 116 | # the sidebar. 117 | # html_logo = '.static/logo_hi.gif' 118 | 119 | # The name of an image file (within the static path) to use as favicon of 120 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 121 | # 32x32 pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) 125 | # here, relative to this directory. They are copied after the builtin 126 | # static files, so a file named "default.css" will overwrite the builtin 127 | # "default.css". 128 | #html_static_path = ['.static'] 129 | 130 | # If not '', a 'Last updated on:' timestamp is inserted at every page 131 | # bottom, using the given strftime format. 132 | html_last_updated_fmt = '%b %d, %Y' 133 | 134 | # If true, SmartyPants will be used to convert quotes and dashes to 135 | # typographically correct entities. 136 | #html_use_smartypants = True 137 | 138 | # Custom sidebar templates, maps document names to template names. 139 | #html_sidebars = {} 140 | 141 | # Additional templates that should be rendered to pages, maps page names to 142 | # template names. 143 | #html_additional_pages = {} 144 | 145 | # If false, no module index is generated. 146 | #html_use_modindex = True 147 | 148 | # If false, no index is generated. 149 | #html_use_index = True 150 | 151 | # If true, the index is split into individual pages for each letter. 152 | #html_split_index = False 153 | 154 | # If true, the reST sources are included in the HTML build as 155 | # _sources/. 156 | #html_copy_source = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages 159 | # will contain a tag referring to it. The value of this option must 160 | # be the base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = '' 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'atemplatedoc' 168 | 169 | 170 | # Options for LaTeX output 171 | # ------------------------ 172 | 173 | # The paper size ('letter' or 'a4'). 174 | #latex_paper_size = 'letter' 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #latex_font_size = '10pt' 178 | 179 | # Grouping the document tree into LaTeX files. List of tuples 180 | # (source start file, target name, title, 181 | # author, document class [howto/manual]). 182 | latex_documents = [ 183 | ('index', 'pyramid_exclog.tex', 'pyramid_exclog Documentation', 184 | 'Repoze Developers', 'manual'), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the 188 | # top of the title page. 189 | latex_logo = '.static/logo_hi.gif' 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are 192 | # parts, not chapters. 193 | #latex_use_parts = False 194 | 195 | # Additional stuff for the LaTeX preamble. 196 | #latex_preamble = '' 197 | 198 | # Documents to append as an appendix to all manuals. 199 | #latex_appendices = [] 200 | 201 | # If false, no module index is generated. 202 | #latex_use_modindex = True 203 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | Pyramid 10 | A `web framework `_. 11 | 12 | tween 13 | A bit of code that sits between the Pyramid router's main request 14 | handling function and the upstream WSGI component that uses Pyramid as 15 | its 'app'. The word "tween" is a contraction of "between". A tween may 16 | be used by Pyramid framework extensions, to provide, for example, 17 | Pyramid-specific view timing support, bookkeeping code that examines 18 | exceptions before they are returned to the upstream WSGI application, or 19 | a variety of other features. Tweens behave a bit like WSGI 'middleware' 20 | but they have the benefit of running in a context in which they have 21 | access to the Pyramid application registry as well as the Pyramid 22 | rendering machinery. See the main Pyramid documentation for more 23 | information about tweens. 24 | 25 | Exception view 26 | An exception view is a :Pyramid view callable which may be invoked when 27 | an exception is raised during request processing. 28 | 29 | Logger 30 | A Python "standard library" ``logging`` module logger. See 31 | https://docs.python.org/library/logging.html for more information about 32 | Python standard library logging. 33 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | pyramid_exclog 3 | ============== 4 | 5 | Overview 6 | ======== 7 | 8 | A package which logs Pyramid application exception (error) information to a 9 | standard Python :term:`logger`. This add-on is most useful when used in 10 | production applications, because the logger can be configured to log to a 11 | file, to UNIX syslog, to the Windows Event Log, or even to email. 12 | 13 | .. warning:: This package will only work with Pyramid 1.5 and better. 14 | 15 | Installation 16 | ============ 17 | 18 | Stable release 19 | -------------- 20 | 21 | To install pyramid_exclog, run this command in your terminal: 22 | 23 | .. code-block:: console 24 | 25 | $ pip install pyramid_exclog 26 | 27 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 28 | you through the process. 29 | 30 | .. _pip: https://pip.pypa.io 31 | .. _Python installation guide: https://docs.python-guide.org/en/latest/starting/installation/ 32 | 33 | 34 | From sources 35 | ------------ 36 | 37 | The sources for pyramid_exclog can be downloaded from the `Github repo`_. 38 | 39 | .. code-block:: console 40 | 41 | $ git clone https://github.com/Pylons/pyramid_exclog.git 42 | 43 | Once you have a copy of the source, you can install it with: 44 | 45 | .. code-block:: console 46 | 47 | $ pip install -e . 48 | 49 | .. _Github repo: https://github.com/Pylons/pyramid_exclog 50 | 51 | Setup 52 | ===== 53 | 54 | Once ``pyramid_exclog`` is installed, you must use the ``config.include`` 55 | mechanism to include it into your Pyramid project's configuration. In your 56 | Pyramid project's ``__init__.py``: 57 | 58 | .. code-block:: python 59 | :linenos: 60 | 61 | config = Configurator(...) 62 | config.include('pyramid_exclog') 63 | 64 | Alternately you can use the ``pyramid.includes`` configuration value in your 65 | ``.ini`` file: 66 | 67 | .. code-block:: ini 68 | :linenos: 69 | 70 | [app:myapp] 71 | pyramid.includes = pyramid_exclog 72 | 73 | Using 74 | ===== 75 | 76 | When this add-on is included into your Pyramid application, whenever a 77 | request to your application causes an exception to be raised, the add-on will 78 | send the URL that caused the exception, the exception type, and its related 79 | traceback information to a standard Python :term:`logger` named 80 | ``exc_logger``. 81 | 82 | You can use the logging configuration in your Pyramid application's ``.ini`` 83 | file to add a logger named ``exc_logger``. This logger should be hooked up a 84 | particular logging handler, which will allow you to use the standard Python 85 | logging machinery to send your exceptions to a file, to syslog, or to an 86 | email address. 87 | 88 | It's not generally useful to add exception logger configuration to a 89 | ``development.ini`` file, because typically exceptions are displayed in the 90 | interactive debugger and to the console which started the application, and 91 | you really don't care much about actually *logging* the exception 92 | information. However, it's very appropriate to add exception logger 93 | configuration to a ``production.ini`` file. 94 | 95 | The following logging configuration statements are in the *default* 96 | ``production.ini`` file generated by all Pyramid scaffolding: 97 | 98 | .. code-block:: ini 99 | :linenos: 100 | 101 | # Begin logging configuration 102 | 103 | [loggers] 104 | keys = root, myapp 105 | 106 | [handlers] 107 | keys = console 108 | 109 | [formatters] 110 | keys = generic 111 | 112 | [logger_root] 113 | level = WARN 114 | handlers = console 115 | 116 | [logger_myapp] 117 | level = WARN 118 | handlers = 119 | qualname = myapp 120 | 121 | [handler_console] 122 | class = StreamHandler 123 | args = (sys.stderr,) 124 | level = NOTSET 125 | formatter = generic 126 | 127 | [formatter_generic] 128 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 129 | 130 | # End logging configuration 131 | 132 | The standard logging configuration of the ``production.ini`` of a scaffolded 133 | Pyramid application does not name a logger named ``exc_logger``. Therefore, 134 | to start making use of ``pyramid_exclog``, you'll have to add an 135 | ``exc_logger`` logger to the configuration. To do so: 136 | 137 | 1) Append ``, exc_logger`` to the ``keys`` value of the ``[loggers]`` section, 138 | 139 | 2) Append ``, exc_handler`` to the ``keys`` value of the ``[handlers]`` 140 | section. 141 | 142 | 3) Append ``, exc_formatter`` to the ``keys`` value of the ``[formatters]`` 143 | section. 144 | 145 | 4) Add a section named ``[logger_exc_logger]`` with logger information 146 | related to the new exception logger. 147 | 148 | 5) Add a section named ``[handler_exc_handler]`` with handler information 149 | related to the new exception logger. In our example, it will have 150 | configuration that tells it to log to a file in the same directory as the 151 | ``.ini`` file named ``exceptions.log``. 152 | 153 | 6) Add a section named ``[formatter_exc_formatter]`` with message formatting 154 | information related to the messages sent to the ``exc_handler`` handler. 155 | By default we'll send only the time and the message. 156 | 157 | The resulting configuration will look like this: 158 | 159 | .. code-block:: ini 160 | :linenos: 161 | 162 | # Begin logging configuration 163 | 164 | [loggers] 165 | keys = root, myapp, exc_logger 166 | 167 | [handlers] 168 | keys = console, exc_handler 169 | 170 | [formatters] 171 | keys = generic, exc_formatter 172 | 173 | [logger_root] 174 | level = WARN 175 | handlers = console 176 | 177 | [logger_myapp] 178 | level = WARN 179 | handlers = 180 | qualname = myapp 181 | 182 | [logger_exc_logger] 183 | level = ERROR 184 | handlers = exc_handler 185 | qualname = exc_logger 186 | 187 | [handler_console] 188 | class = StreamHandler 189 | args = (sys.stderr,) 190 | level = NOTSET 191 | formatter = generic 192 | 193 | [handler_exc_handler] 194 | class = FileHandler 195 | args = ('%(here)s/exceptions.log',) 196 | level = ERROR 197 | formatter = exc_formatter 198 | 199 | [formatter_generic] 200 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 201 | 202 | [formatter_exc_formatter] 203 | format = %(asctime)s %(message)s 204 | 205 | # End logging configuration 206 | 207 | Once you've changed your logging configuration as per the above, and you 208 | restart your Pyramid application, all exceptions will be logged to a file 209 | named ``exceptions.log`` in the directory that the ``production.ini`` file 210 | lives. 211 | 212 | You can get fancier with logging as necessary by familiarizing yourself with 213 | the Python ``logging`` module configuration format. For example, here's an 214 | alternate configuration that logs exceptions via email to a user named 215 | ``from@example.com`` to a user named ``to@example.com`` via the SMTP server 216 | on the local host at port 25; each email will have the subject ``myapp 217 | Exception``: 218 | 219 | .. code-block:: ini 220 | :linenos: 221 | 222 | # Begin logging configuration 223 | 224 | [loggers] 225 | keys = root, myapp, exc_logger 226 | 227 | [handlers] 228 | keys = console, exc_handler 229 | 230 | [formatters] 231 | keys = generic, exc_formatter 232 | 233 | [logger_root] 234 | level = WARN 235 | handlers = console 236 | 237 | [logger_myapp] 238 | level = WARN 239 | handlers = 240 | qualname = myapp 241 | 242 | [logger_exc_logger] 243 | level = ERROR 244 | handlers = exc_handler 245 | qualname = exc_logger 246 | 247 | [handler_console] 248 | class = StreamHandler 249 | args = (sys.stderr,) 250 | level = NOTSET 251 | formatter = generic 252 | 253 | [handler_exc_handler] 254 | class = handlers.SMTPHandler 255 | args = (('localhost', 25), 'from@example.com', ['to@example.com'], 'myapp Exception') 256 | level = ERROR 257 | formatter = exc_formatter 258 | 259 | [formatter_generic] 260 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 261 | 262 | [formatter_exc_formatter] 263 | format = %(asctime)s %(message)s 264 | 265 | # End logging configuration 266 | 267 | When the above configuration is used, exceptions will be sent via email 268 | instead of sent to a file. 269 | 270 | For information about logging in general see the `Pythong logging module 271 | documentation `_. Practical 272 | tips are contained within the `Python logging cookbook 273 | `_. 274 | More information about the the ``.ini`` logging file configuration format is 275 | at 276 | https://docs.python.org/library/logging.config.html#configuration-file-format 277 | . 278 | 279 | Settings 280 | ========= 281 | 282 | :mod:`pyramid_exclog`` also has some its own settings in the form of 283 | configuration values which are meant to be placed in the ``[app:myapp]`` 284 | section of your Pyramid's ``.ini`` file. These are: 285 | 286 | ``exclog.ignore`` 287 | 288 | By default, the exception logging machinery will log all exceptions (even 289 | those eventually caught by a Pyramid :term:`exception view`) except "http 290 | exceptions" (any exception that derives from the base class 291 | ``pyramid.httpexceptions.WSGIHTTPException`` such as ``HTTPFound``). You 292 | can instruct ``pyramid_exclog`` to override this default in order to 293 | ignore custom exception types (or to re-enable logging "http exceptions") 294 | by using the ``excview.ignore`` configuration setting. 295 | 296 | ``excview.ignore`` is a list of dotted Python names representing exception 297 | types (e.g. ``myapp.MyException``) or builtin exception names (e.g. 298 | ``NotImplementedError`` or ``KeyError``) that represent exceptions which 299 | should never be logged. This list can be in the form of a 300 | whitespace-separated string, e.g. ``KeyError ValueError 301 | myapp.MyException`` or it may consume multiple lines in the ``.ini`` file. 302 | 303 | This setting defaults to a list containing only 304 | ``pyramid.httpexceptions.WSGIHTTPException``. 305 | 306 | An example: 307 | 308 | .. code-block:: ini 309 | 310 | [app:myapp] 311 | exclog.ignore = pyramid.httpexceptions.WSGIHTTPException 312 | KeyError 313 | myapp.exceptions.MyException 314 | 315 | ``exclog.extra_info`` 316 | 317 | By default the only content in error messages is the URL that was 318 | accessed (retrieved from the url attribute of ``pyramid.request.Request``) 319 | and the exception information that is appended by Python's 320 | ``Logger.exception`` function. 321 | 322 | If ``exclog.extra_info`` is true the error message will also include 323 | the environ and params attributes of ``pyramid.request.Request`` formatted 324 | using ``pprint.pformat()``. The output from 325 | ``pyramid.security.unauthenticated_id()`` is also included. 326 | 327 | This setting defaults to false 328 | 329 | An example: 330 | 331 | .. code-block:: ini 332 | 333 | [app:myapp] 334 | exclog.extra_info = true 335 | 336 | ``exclog.get_message`` 337 | 338 | If a customized error message is needed, the ``exclog.get_message`` 339 | setting can be pointed at a function that takes a request as its only 340 | argument and returns a string. It can be either a dotted name or the 341 | actual function. For example: 342 | 343 | .. code-block:: ini 344 | 345 | [app:myapp] 346 | exclog.get_message = myapp.somemodule.get_exc_log_message 347 | 348 | If ``exclog.get_message`` is set, ``exclog.extra_info`` will be ignored. 349 | 350 | ``exclog.hidden_cookies`` 351 | 352 | A list of keys of cookies to hide in the error message. The cookie's value 353 | will be replaced with "hidden", so you can still tell whether the cookie was 354 | present. 355 | 356 | This works with either ``exclog.extra_info`` or ``exclog.get_message``. If 357 | ``exclog.hidden_cookies`` is set, then any function specified in 358 | ``exclog.get_message`` will receive a copy of the request with the cookies 359 | already replaced. 360 | 361 | An example: 362 | 363 | .. code-block:: ini 364 | 365 | [app:myapp] 366 | exclog.hidden_cookies = auth_tkt 367 | another_cookie 368 | 369 | Explicit "Tween" Configuration 370 | ============================== 371 | 372 | Note that the exception logger is implemented as a Pyramid :term:`tween`, and 373 | it can be used in the explicit tween chain if its implicit position in the 374 | tween chain is incorrect (see the output of ``ptweens``):: 375 | 376 | [app:myapp] 377 | pyramid.tweens = someothertween 378 | pyramid_exclog.exclog_tween_factory 379 | pyramid.tweens.excview_tween_factory 380 | 381 | It usually belongs directly above the ``pyramid.tweens.excview_tween_factory`` 382 | entry in the ``ptweens`` output, and will attempt to sort there by default as 383 | the result of having ``config.include('pyramid_exclog')`` invoked. 384 | 385 | Deployment under mod_wsgi 386 | ========================= 387 | 388 | To make logging facilities available when loading an application via 389 | mod_wsgi, like it behaves with pserve, you must call the ``logging.fileConfig`` 390 | function on the ini file containing the logger entry. 391 | 392 | Here's an example of a ``run.wsgi`` file: 393 | 394 | .. code-block:: python 395 | 396 | import os 397 | from pyramid.paster import get_app, setup_logging 398 | 399 | here = os.path.dirname(os.path.abspath(__file__)) 400 | conf = os.path.join(here, 'production.ini') 401 | setup_logging(conf) 402 | 403 | application = get_app(conf, 'main') 404 | 405 | More Information 406 | ================ 407 | 408 | .. toctree:: 409 | :maxdepth: 1 410 | 411 | api.rst 412 | glossary.rst 413 | 414 | 415 | Reporting Bugs / Development Versions 416 | ===================================== 417 | 418 | Visit https://github.com/Pylons/pyramid_exclog to download development or 419 | tagged versions. 420 | 421 | Visit https://github.com/Pylons/pyramid_exclog/issues to report bugs. 422 | 423 | Indices and tables 424 | ------------------ 425 | 426 | * :ref:`glossary` 427 | * :ref:`genindex` 428 | * :ref:`modindex` 429 | * :ref:`search` 430 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 41", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 79 7 | skip-string-normalization = true 8 | target-version = ['py37', 'py38', 'py39', 'py310'] 9 | exclude = ''' 10 | /( 11 | \.git 12 | | \.mypy_cache 13 | | \.tox 14 | | \.venv 15 | | \.pytest_cache 16 | | dist 17 | | build 18 | | docs 19 | )/ 20 | ''' 21 | 22 | # This next section only exists for people that have their editors 23 | # automatically call isort, black already sorts entries on its own when run. 24 | [tool.isort] 25 | profile = "black" 26 | line_length = 79 27 | force_sort_within_sections = true 28 | sections = "FUTURE,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" 29 | known_first_party = "pyramid_exclog" 30 | -------------------------------------------------------------------------------- /rtd.txt: -------------------------------------------------------------------------------- 1 | -e .[docs] 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyramid_exclog 3 | version = 1.1 4 | description = A package which logs to a Python logger when an exception is raised by a Pyramid application 5 | long_description = file: README.rst, CHANGES.rst 6 | long_description_content_type = text/x-rst 7 | keywords = wsgi pylons pyramid mail tween exception handler 8 | license = BSD-derived (Repoze) 9 | license_file = LICENSE.txt 10 | classifiers = 11 | Intended Audience :: Developers 12 | Programming Language :: Python 13 | Programming Language :: Python :: 3 14 | Programming Language :: Python :: 3.7 15 | Programming Language :: Python :: 3.8 16 | Programming Language :: Python :: 3.9 17 | Programming Language :: Python :: 3.10 18 | Framework :: Pyramid 19 | Topic :: Internet :: WWW/HTTP :: WSGI 20 | License :: Repoze Public License 21 | url = https://github.com/Pylons/pyramid_exclog 22 | project_urls = 23 | Documentation = https://docs.pylonsproject.org/projects/pyramid-exclog/en/latest/index.html 24 | Changelog = https://docs.pylonsproject.org/projects/pyramid-exclog/en/latest/changes.html 25 | Issue Tracker = https://github.com/Pylons/pyramid_exclog/issues 26 | author = Chris McDonough 27 | author_email = pylons-discuss@googlegroups.com 28 | maintainer = Pylons Project 29 | maintainer_email = pylons-discuss@googlegroups.com 30 | 31 | [options] 32 | package_dir= 33 | =src 34 | packages = find: 35 | include_package_data = True 36 | python_requires = >=3.7 37 | install_requires = 38 | pyramid >= 1.5 39 | 40 | [options.packages.find] 41 | where = src 42 | 43 | [options.extras_require] 44 | testing = 45 | pytest 46 | pytest-cov 47 | coverage>=5.0 48 | 49 | docs = 50 | docutils 51 | Sphinx>=1.8.1 52 | pylons-sphinx-themes>=1.0.9 53 | 54 | [bdist_wheel] 55 | universal = 1 56 | 57 | [tool:pytest] 58 | python_files = test_*.py 59 | testpaths = 60 | tests 61 | addopts = -W always --cov 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/pyramid_exclog/__init__.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | from pprint import pformat 3 | from pyramid.httpexceptions import WSGIHTTPException 4 | from pyramid.settings import asbool, aslist 5 | import pyramid.tweens 6 | from pyramid.util import DottedNameResolver 7 | import sys 8 | 9 | resolver = DottedNameResolver(None) 10 | 11 | 12 | def as_globals_list(value): 13 | L = [] 14 | value = aslist(value) 15 | for dottedname in value: 16 | if dottedname in builtins.__dict__: 17 | dottedname = 'builtins.%s' % dottedname 18 | obj = resolver.maybe_resolve(dottedname) 19 | L.append(obj) 20 | return L 21 | 22 | 23 | def _get_url(request): 24 | try: 25 | url = repr(request.url) 26 | except UnicodeDecodeError: 27 | # do the best we can 28 | url = ( 29 | request.host_url 30 | + request.environ.get('SCRIPT_NAME') 31 | + request.environ.get('PATH_INFO') 32 | ) 33 | qs = request.environ.get('QUERY_STRING') 34 | if qs: 35 | url += '?' + qs 36 | url = 'could not decode url: %r' % url 37 | return url 38 | 39 | 40 | _MESSAGE_TEMPLATE = """ 41 | 42 | %(url)s 43 | 44 | ENVIRONMENT 45 | 46 | %(env)s 47 | 48 | 49 | PARAMETERS 50 | 51 | %(params)s 52 | 53 | 54 | UNAUTHENTICATED USER 55 | 56 | %(usr)s 57 | 58 | """ 59 | 60 | 61 | def _hide_cookies(cookie_keys, request): 62 | """ 63 | Return a copy of the request with the specified cookies' values replaced 64 | with "hidden", if present. 65 | """ 66 | 67 | new_request = request.copy() 68 | new_request.registry = request.registry 69 | cookies = new_request.cookies 70 | 71 | for key in cookie_keys: 72 | if key in cookies: 73 | cookies[key] = 'hidden' 74 | 75 | # This forces the cookie handler to update its parsed cookies cache, which 76 | # also ends up in the environ dump 77 | len(cookies) 78 | 79 | return new_request 80 | 81 | 82 | def _get_message(request): 83 | """ 84 | Return a string with useful information from the request. 85 | 86 | On python 2 this method will return ``unicode`` and on Python 3 ``str`` 87 | will be returned. This seems to be what the logging module expects. 88 | 89 | """ 90 | url = _get_url(request) 91 | unauth = request.unauthenticated_userid 92 | 93 | try: 94 | params = request.params 95 | except UnicodeDecodeError: 96 | params = 'could not decode params' 97 | except IOError as ex: 98 | params = 'IOError while decoding params: %s' % ex 99 | 100 | if not isinstance(unauth, str): 101 | unauth = repr(unauth) 102 | 103 | return _MESSAGE_TEMPLATE % dict( 104 | url=url, 105 | env=pformat(request.environ), 106 | params=pformat(params), 107 | usr=unauth, 108 | ) 109 | 110 | 111 | class ErrorHandler(object): 112 | def __init__(self, ignored, getLogger, get_message, hidden_cookies=()): 113 | self.ignored = ignored 114 | self.getLogger = getLogger 115 | self.get_message = get_message 116 | self.hidden_cookies = hidden_cookies 117 | 118 | def __call__(self, request, exc_info=None): 119 | # save the traceback as it may get lost when we get the message. 120 | # _handle_error is not in the traceback, so calling sys.exc_info 121 | # does NOT create a circular reference 122 | if exc_info is None: 123 | exc_info = sys.exc_info() 124 | 125 | if isinstance(exc_info[1], self.ignored): 126 | return 127 | try: 128 | if self.hidden_cookies: 129 | request = _hide_cookies(self.hidden_cookies, request) 130 | 131 | logger = self.getLogger('exc_logger') 132 | message = self.get_message(request) 133 | logger.error(message, exc_info=exc_info) 134 | except BaseException: 135 | logger.exception("Exception while logging") 136 | 137 | 138 | def exclog_tween_factory(handler, registry): 139 | get = registry.settings.get 140 | 141 | ignored = get('exclog.ignore', (WSGIHTTPException,)) 142 | get_message = _get_url 143 | if get('exclog.extra_info', False): 144 | get_message = _get_message 145 | get_message = get('exclog.get_message', get_message) 146 | hidden_cookies = get('exclog.hidden_cookies', ()) 147 | 148 | getLogger = get('exclog.getLogger', 'logging.getLogger') 149 | getLogger = resolver.maybe_resolve(getLogger) 150 | 151 | handle_error = ErrorHandler( 152 | ignored, getLogger, get_message, hidden_cookies=hidden_cookies 153 | ) 154 | 155 | def exclog_tween(request): 156 | try: 157 | response = handler(request) 158 | exc_info = getattr(request, 'exc_info', None) 159 | if exc_info is not None: 160 | handle_error(request, exc_info) 161 | return response 162 | 163 | except BaseException: 164 | handle_error(request) 165 | raise 166 | 167 | return exclog_tween 168 | 169 | 170 | def includeme(config): 171 | """ 172 | Set up am implicit :term:`tween` to log exception information that is 173 | generated by your Pyramid application. The logging data will be sent to 174 | the Python logger named ``exc_logger``. 175 | 176 | This tween configured to be placed 'over' the exception view tween. It 177 | will log all exceptions (even those caught by a Pyramid exception view) 178 | except 'http exceptions' (any exception that derives from 179 | ``pyramid.httpexceptions.WSGIHTTPException`` such as ``HTTPFound``). You 180 | can instruct ``pyramid_exclog`` to ignore custom exception types by using 181 | the ``exclog.ignore`` configuration setting. 182 | 183 | """ 184 | get = config.registry.settings.get 185 | ignored = as_globals_list( 186 | get( 187 | 'exclog.ignore', 188 | 'pyramid.httpexceptions.WSGIHTTPException', 189 | ) 190 | ) 191 | extra_info = asbool(get('exclog.extra_info', False)) 192 | hidden_cookies = aslist(get('exclog.hidden_cookies', '')) 193 | get_message = get('exclog.get_message', None) 194 | if get_message is not None: 195 | get_message = config.maybe_dotted(get_message) 196 | config.registry.settings['exclog.get_message'] = get_message 197 | config.registry.settings['exclog.ignore'] = tuple(ignored) 198 | config.registry.settings['exclog.extra_info'] = extra_info 199 | config.registry.settings['exclog.hidden_cookies'] = hidden_cookies 200 | config.add_tween( 201 | 'pyramid_exclog.exclog_tween_factory', 202 | over=[ 203 | pyramid.tweens.EXCVIEW, 204 | # if pyramid_tm is in the pipeline we want to track errors caused 205 | # by commit/abort so we try to place ourselves over it 206 | 'pyramid_tm.tm_tween_factory', 207 | ], 208 | ) 209 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_exclog/7c3abd130890d45578866ed08944d351f863135f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_it.py: -------------------------------------------------------------------------------- 1 | from pyramid import testing 2 | import sys 3 | import unittest 4 | 5 | 6 | class Test_exclog_tween_factory(unittest.TestCase): 7 | def setUp(self): 8 | self.config = testing.setUp() 9 | self.registry = self.config.registry 10 | 11 | def _callFUT(self): 12 | from pyramid_exclog import exclog_tween_factory 13 | 14 | return exclog_tween_factory(None, self.registry) 15 | 16 | def test_no_recipients(self): 17 | handler = self._callFUT() 18 | self.assertEqual(handler.__name__, 'exclog_tween') 19 | 20 | 21 | class Test_exclog_tween(unittest.TestCase): 22 | def setUp(self): 23 | self.request = request = _request_factory('/') 24 | self.config = testing.setUp(request=request) 25 | self.registry = self.config.registry 26 | self.registry.settings = {} 27 | request.registry = self.registry 28 | self.logger = DummyLogger() 29 | 30 | def getLogger(self, name): 31 | self.logger.name = name 32 | return self.logger 33 | 34 | def handler(self, request): 35 | raise NotImplementedError 36 | 37 | def _callFUT( 38 | self, handler=None, registry=None, request=None, getLogger=None 39 | ): 40 | from pyramid_exclog import exclog_tween_factory 41 | 42 | if handler is None: 43 | handler = self.handler 44 | if registry is None: 45 | registry = self.registry 46 | if request is None: 47 | request = self.request 48 | if getLogger is None: 49 | getLogger = self.getLogger 50 | registry.settings['exclog.getLogger'] = getLogger 51 | tween = exclog_tween_factory(handler, registry) 52 | return tween(request) 53 | 54 | def test_ignored(self): 55 | self.registry.settings['exclog.ignore'] = (NotImplementedError,) 56 | self.assertRaises(NotImplementedError, self._callFUT) 57 | self.assertEqual(len(self.logger.exceptions), 0) 58 | 59 | def test_notignored(self): 60 | self.assertRaises(NotImplementedError, self._callFUT) 61 | self.assertEqual(len(self.logger.exceptions), 1) 62 | msg = self.logger.exceptions[0] 63 | self.assertEqual(msg, repr(self.request.url)) 64 | 65 | def test_exc_info(self): 66 | def handler(request): 67 | try: 68 | raise NotImplementedError 69 | except Exception as ex: 70 | exc_info = sys.exc_info() 71 | try: 72 | request.exception = ex 73 | request.exc_info = exc_info 74 | finally: 75 | del exc_info 76 | return b'dummy response' 77 | 78 | result = self._callFUT(handler=handler) 79 | self.assertEqual(b'dummy response', result) 80 | self.assertEqual(len(self.logger.exceptions), 1) 81 | msg = self.logger.exceptions[0] 82 | self.assertEqual(msg, repr(self.request.url)) 83 | 84 | def test_extra_info(self): 85 | self.registry.settings['exclog.extra_info'] = True 86 | self.assertRaises(NotImplementedError, self._callFUT) 87 | self.assertEqual(len(self.logger.exceptions), 1) 88 | msg = self.logger.exceptions[0].strip() 89 | self.assertTrue( 90 | msg.startswith("'http://localhost/'\n\nENVIRONMENT"), msg 91 | ) 92 | self.assertTrue("PARAMETERS\n\nNestedMultiDict([])" in msg) 93 | self.assertTrue('ENVIRONMENT' in msg) 94 | 95 | def test_get_message(self): 96 | self.registry.settings['exclog.get_message'] = lambda req: 'MESSAGE' 97 | self.assertRaises(NotImplementedError, self._callFUT) 98 | self.assertEqual(len(self.logger.exceptions), 1) 99 | msg = self.logger.exceptions[0] 100 | self.assertEqual(msg, 'MESSAGE') 101 | 102 | def test_hidden_cookies(self): 103 | self.registry.settings['exclog.extra_info'] = True 104 | self.registry.settings['exclog.hidden_cookies'] = ['test_cookie'] 105 | self.request.cookies['test_cookie'] = 'test_cookie_value' 106 | self.assertRaises(NotImplementedError, self._callFUT) 107 | msg = self.logger.exceptions[0] 108 | self.assertTrue('test_cookie=hidden' in msg, msg) 109 | self.assertFalse('test_cookie_value' in msg) 110 | 111 | def test_user_info_user(self): 112 | self.config.testing_securitypolicy(userid='hank', permissive=True) 113 | self.registry.settings['exclog.extra_info'] = True 114 | self.assertRaises(NotImplementedError, self._callFUT) 115 | self.assertEqual(len(self.logger.exceptions), 1) 116 | msg = self.logger.exceptions[0] 117 | self.assertTrue('UNAUTHENTICATED USER\n\nhank' in msg, msg) 118 | 119 | def test_user_info_no_user(self): 120 | self.registry.settings['exclog.extra_info'] = True 121 | self.assertRaises(NotImplementedError, self._callFUT) 122 | self.assertEqual(len(self.logger.exceptions), 1) 123 | msg = self.logger.exceptions[0] 124 | self.assertTrue('UNAUTHENTICATED USER\n\nNone\n' in msg, msg) 125 | 126 | def test_exception_while_logging(self): 127 | from pyramid.request import Request 128 | 129 | bang = AssertionError('bang') 130 | 131 | class BadRequest(Request): 132 | @property 133 | def url(self): 134 | raise bang 135 | 136 | request = _request_factory('/', request_class=BadRequest) 137 | self.assertRaises(Exception, self._callFUT, request=request) 138 | msg = self.logger.exceptions[0] 139 | self.assertEqual('Exception while logging', msg) 140 | raised = self.logger.exc_info[0][1] 141 | self.assertEqual(raised, bang) 142 | 143 | 144 | class Test__get_url(unittest.TestCase): 145 | def _callFUT(self, request): 146 | from pyramid_exclog import _get_url 147 | 148 | return _get_url(request) 149 | 150 | def test_normal(self): 151 | from pyramid.testing import DummyRequest 152 | 153 | request = DummyRequest() 154 | self.assertEqual(self._callFUT(request), "'http://example.com'") 155 | 156 | def test_w_deocode_error_wo_qs(self): 157 | request = _request_factory('/') 158 | request.environ['SCRIPT_NAME'] = '/script' 159 | request.environ['PATH_INFO'] = '/path/with/latin1/\x80' 160 | self.assertEqual( 161 | self._callFUT(request), 162 | r"could not decode url: 'http://localhost/script/path/with/latin1/\x80'", 163 | ) 164 | 165 | def test_w_deocode_error_w_qs(self): 166 | request = _request_factory('/') 167 | request.environ['SCRIPT_NAME'] = '/script' 168 | request.environ['PATH_INFO'] = '/path/with/latin1/\x80' 169 | request.environ['QUERY_STRING'] = 'foo=bar' 170 | self.assertEqual( 171 | self._callFUT(request), 172 | r"could not decode url: 'http://localhost/script/path/with/latin1/\x80" 173 | r"?foo=bar'", 174 | ) 175 | 176 | 177 | class Test__get_message(unittest.TestCase): 178 | def _callFUT(self, request): 179 | from pyramid_exclog import _get_message 180 | 181 | return _get_message(request) 182 | 183 | def test_evil_encodings(self): 184 | request = _request_factory('/%FA') # not utf-8 185 | msg = self._callFUT(request) 186 | self.assertTrue("could not decode url: 'http://localhost/" in msg) 187 | 188 | def test_return_type_is_unicode(self): 189 | # _get_message should return something the logging module accepts. 190 | # this, apparently is unicode or ascii-encoded bytes. Unfortunately, 191 | # unicode fails with some handlers if you do not set the encoding 192 | # on them. 193 | request = _request_factory('/url') # not utf-8 194 | msg = self._callFUT(request) 195 | self.assertTrue(isinstance(msg, str), repr(msg)) 196 | 197 | def test_evil_encodings_extra_info(self): 198 | request = _request_factory('/url?%FA=%FA') # not utf-8 199 | msg = self._callFUT(request) 200 | self.assertTrue("could not decode params" in msg, msg) 201 | 202 | def test_unicode_user_id_with_non_utf_8_url(self): 203 | # On Python 2 we may get a unicode userid while QUERY_STRING is a "str" 204 | # object containing non-ascii bytes. 205 | with testing.testConfig() as config: 206 | config.testing_securitypolicy( 207 | userid=b'\xe6\xbc\xa2'.decode('utf-8') 208 | ) 209 | request = _request_factory('/') 210 | request.environ['PATH_INFO'] = '/url' 211 | request.environ['QUERY_STRING'] = '\xfa=\xfa' 212 | msg = self._callFUT(request) 213 | self.assertTrue("could not decode params" in msg, msg) 214 | 215 | def test_non_ascii_bytes_in_userid(self): 216 | byte_str = b'\xe6\xbc\xa2' 217 | with testing.testConfig() as config: 218 | config.testing_securitypolicy(userid=byte_str) 219 | request = _request_factory('/') 220 | msg = self._callFUT(request) 221 | self.assertTrue(repr(byte_str) in msg, msg) 222 | 223 | def test_integer_user_id(self): 224 | # userids can apparently be integers as well 225 | with testing.testConfig() as config: 226 | config.testing_securitypolicy(userid=42) 227 | request = _request_factory('/') 228 | msg = self._callFUT(request) 229 | self.assertTrue('42' in msg) 230 | 231 | def test_evil_encodings_extra_info_POST(self): 232 | request = _request_factory( 233 | '/url', 234 | content_type='application/x-www-form-urlencoded; charset=utf-8', 235 | POST='%FA=%FA', 236 | ) # not utf-8 237 | self._callFUT(request) # doesn't fail 238 | 239 | def test_io_error(self): 240 | from pyramid.request import Request 241 | 242 | bang = IOError('bang') 243 | 244 | class BadRequest(Request): 245 | @property 246 | def params(self): 247 | raise bang 248 | 249 | request = _request_factory('/', request_class=BadRequest) 250 | msg = self._callFUT(request) 251 | self.assertTrue("IOError while decoding params: bang" in msg, msg) 252 | 253 | 254 | class Test_includeme(unittest.TestCase): 255 | def _callFUT(self, config): 256 | from pyramid_exclog import includeme 257 | 258 | return includeme(config) 259 | 260 | def test_it(self): 261 | from pyramid.httpexceptions import WSGIHTTPException 262 | from pyramid.tweens import EXCVIEW 263 | 264 | config = DummyConfig() 265 | self._callFUT(config) 266 | self.assertEqual( 267 | config.tweens, 268 | [ 269 | ( 270 | 'pyramid_exclog.exclog_tween_factory', 271 | None, 272 | [EXCVIEW, 'pyramid_tm.tm_tween_factory'], 273 | ) 274 | ], 275 | ) 276 | self.assertEqual( 277 | config.registry.settings['exclog.ignore'], (WSGIHTTPException,) 278 | ) 279 | 280 | def test_it_withignored_builtin(self): 281 | config = DummyConfig() 282 | config.settings['exclog.ignore'] = 'NotImplementedError' 283 | self._callFUT(config) 284 | self.assertEqual( 285 | config.registry.settings['exclog.ignore'], (NotImplementedError,) 286 | ) 287 | 288 | def test_it_withignored_nonbuiltin(self): 289 | config = DummyConfig() 290 | config.settings['exclog.ignore'] = 'tests.test_it.DummyException' 291 | self._callFUT(config) 292 | self.assertEqual( 293 | config.registry.settings['exclog.ignore'], (DummyException,) 294 | ) 295 | 296 | def test_it_with_extra_info(self): 297 | config = DummyConfig() 298 | config.settings['exclog.extra_info'] = 'true' 299 | self._callFUT(config) 300 | self.assertEqual(config.registry.settings['exclog.extra_info'], True) 301 | 302 | def test_it_with_get_message(self): 303 | config = DummyConfig() 304 | get_message = lambda req: 'MESSAGE' # noqa E722 305 | config.settings['exclog.get_message'] = get_message 306 | self._callFUT(config) 307 | self.assertEqual( 308 | config.registry.settings['exclog.get_message'], get_message 309 | ) 310 | 311 | def test_get_message_not_set_by_includeme(self): 312 | config = DummyConfig() 313 | self._callFUT(config) 314 | self.assertTrue('exclog.get_message' not in config.registry.settings) 315 | 316 | 317 | class DummyException(object): 318 | pass 319 | 320 | 321 | class DummyLogger(object): 322 | def __init__(self): 323 | self.exceptions = [] 324 | self.exc_info = [] 325 | 326 | def error(self, msg, exc_info=None): 327 | self.exceptions.append(msg) 328 | self.exc_info.append(exc_info) 329 | 330 | def exception(self, msg): 331 | self.exceptions.append(msg) 332 | self.exc_info.append(sys.exc_info()) 333 | 334 | 335 | class DummyConfig(object): 336 | def __init__(self): 337 | self.tweens = [] 338 | self.registry = self 339 | self.settings = {} 340 | 341 | def add_tween(self, factory, under=None, over=None): 342 | self.tweens.append((factory, under, over)) 343 | 344 | def maybe_dotted(self, obj): 345 | """NOTE: ``obj`` should NOT be a dotted name.""" 346 | return obj 347 | 348 | 349 | def _request_factory(*args, **kwargs): 350 | """Construct a request object for testing 351 | 352 | This will pass on any args and kwargs to the specified class instance. 353 | 354 | :param request_class: Specific class to use to create the request object 355 | :return: An instantiated version of the request class for testing 356 | """ 357 | from pyramid.request import Request 358 | from pyramid.threadlocal import get_current_registry 359 | 360 | request = kwargs.pop("request_class", Request).blank(*args, **kwargs) 361 | 362 | # Pyramid 2.0 does not appear to attach a registry by default which will 363 | # lead to crashes we aren't looking for. 364 | request.registry = get_current_registry() 365 | 366 | return request 367 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | lint, 4 | py37,py38,py39,py310,pypy3, 5 | py37-pyramid{15,16,17,18,19,110}, 6 | docs, 7 | coverage 8 | isolated_build = True 9 | 10 | [testenv] 11 | deps = 12 | pyramid15: pyramid <= 1.5.99 13 | pyramid16: pyramid <= 1.6.99 14 | pyramid17: pyramid <= 1.7.99 15 | pyramid18: pyramid <= 1.8.99 16 | pyramid19: pyramid <= 1.9.99 17 | pyramid110: pyramid <= 1.10.99 18 | commands = 19 | python --version 20 | pytest {posargs:} 21 | extras = 22 | testing 23 | setenv = 24 | COVERAGE_FILE=.coverage.{envname} 25 | 26 | [testenv:coverage] 27 | skip_install = True 28 | commands = 29 | coverage combine 30 | coverage xml 31 | coverage report --fail-under=100 32 | deps = 33 | coverage 34 | setenv = 35 | COVERAGE_FILE=.coverage 36 | depends = py310 37 | 38 | [testenv:lint] 39 | skip_install = True 40 | commands = 41 | isort --check-only --df src/pyramid_exclog tests 42 | black --check --diff . 43 | check-manifest 44 | flake8 src/pyramid_exclog/ tests 45 | # build sdist/wheel 46 | python -m build . 47 | twine check dist/* 48 | deps = 49 | black 50 | build 51 | check-manifest 52 | isort 53 | readme_renderer 54 | twine 55 | flake8 56 | flake8-bugbear 57 | 58 | [testenv:docs] 59 | whitelist_externals = make 60 | commands = 61 | make -C docs html epub BUILDDIR={envdir} 62 | extras = 63 | docs 64 | 65 | [testenv:format] 66 | skip_install = true 67 | commands = 68 | isort src/pyramid_exclog tests 69 | black . 70 | deps = 71 | black 72 | isort 73 | 74 | [testenv:build] 75 | skip_install = true 76 | commands = 77 | # clean up build/ and dist/ folders 78 | python -c 'import shutil; shutil.rmtree("build", ignore_errors=True)' 79 | # Make sure we aren't forgetting anything 80 | check-manifest 81 | # build sdist/wheel 82 | python -m build . 83 | # Verify all is well 84 | twine check dist/* 85 | 86 | deps = 87 | build 88 | check-manifest 89 | readme_renderer 90 | twine 91 | --------------------------------------------------------------------------------