Deprecated: Function get_magic_quotes_gpc() is deprecated in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 99

Deprecated: The each() function is deprecated. This message will be suppressed on further calls in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 619

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 832

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 839
diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 4d91d7f8..00000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -engines: - duplication: - enabled: true - config: - languages: - - python - fixme: - enabled: true - pep8: - enabled: true - radon: - enabled: true -ratings: - paths: - - "**.py" -exclude_paths: [] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57f004a1..59a77d18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,201 +9,78 @@ on: jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: matrix: - python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9, '3.10', 3.11] framework: - - FLASK_VERSION=0.10.1 Werkzeug\>=0.7,\<1.0 - - FLASK_VERSION=0.11.1 Werkzeug\>=0.7,\<1.0 - - FLASK_VERSION=0.12.4 Werkzeug\>=0.7,\<1.0 - - FLASK_VERSION=1.0.2 - - TWISTED_VERSION=15.5.0 treq==15.1.0 zope.interface==4.1.3 - - TWISTED_VERSION=16.1.0 treq==16.12.0 zope.interface==4.1.3 - - TWISTED_VERSION=16.2.0 treq==16.12.0 zope.interface==4.1.3 - - TWISTED_VERSION=16.3.0 treq==16.12.0 zope.interface==4.2.0 - - TWISTED_VERSION=16.4.0 treq==16.12.0 zope.interface==4.5.0 - - TWISTED_VERSION=16.5.0 treq==16.12.0 zope.interface==4.5.0 - - TWISTED_VERSION=16.6.0 treq==16.12.0 zope.interface==4.5.0 - - TWISTED_VERSION=17.1.0 treq==16.12.0 zope.interface==4.5.0 + - FLASK_VERSION=1.1.4 + - FLASK_VERSION=2.2.3 - DJANGO_VERSION=1.11.29 - - DJANGO_VERSION=2.0.13 - - DJANGO_VERSION=2.1.15 - - DJANGO_VERSION=2.2.26 - - DJANGO_VERSION=3.0.14 - - DJANGO_VERSION=3.1.14 - - DJANGO_VERSION=3.2.11 - - DJANGO_VERSION=4.0.1 - - PYRAMID_VERSION=1.9.2 - - PYRAMID_VERSION=1.10.4 - - STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 + - DJANGO_VERSION=2.2.28 + - DJANGO_VERSION=3.2.18 + - DJANGO_VERSION=4.0.10 + - DJANGO_VERSION=4.1.7 + - TWISTED_VERSION=20.3.0 + - TWISTED_VERSION=21.7.0 + - TWISTED_VERSION=22.10.0 + - PYRAMID_VERSION=1.10.8 - STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 - STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 - FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 - FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 - FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 exclude: - - python-version: 2.7 - framework: DJANGO_VERSION=2.0.13 - - python-version: 2.7 - framework: DJANGO_VERSION=2.1.15 - - python-version: 2.7 - framework: DJANGO_VERSION=2.2.26 - - python-version: 2.7 - framework: DJANGO_VERSION=3.0.14 - - python-version: 2.7 - framework: DJANGO_VERSION=3.1.14 - - python-version: 2.7 - framework: DJANGO_VERSION=3.2.11 - - python-version: 2.7 - framework: DJANGO_VERSION=4.0.1 - - python-version: 3.4 - framework: DJANGO_VERSION=2.1.15 - - python-version: 3.4 - framework: DJANGO_VERSION=2.2.26 - - python-version: 3.4 - framework: DJANGO_VERSION=3.0.14 - - python-version: 3.4 - framework: DJANGO_VERSION=3.1.14 - - python-version: 3.4 - framework: DJANGO_VERSION=3.2.11 - - python-version: 3.4 - framework: DJANGO_VERSION=4.0.1 - - python-version: 3.5 - framework: DJANGO_VERSION=3.0.14 - - python-version: 3.5 - framework: DJANGO_VERSION=3.1.14 - - python-version: 3.5 - framework: DJANGO_VERSION=3.2.11 - - python-version: 3.5 - framework: DJANGO_VERSION=4.0.1 - - python-version: 3.6 - framework: DJANGO_VERSION=4.0.1 - - python-version: 3.7 - framework: DJANGO_VERSION=4.0.1 - - python-version: 3.8 - framework: DJANGO_VERSION=1.11.29 - - python-version: 3.8 - framework: DJANGO_VERSION=2.0.13 - - python-version: 3.8 - framework: DJANGO_VERSION=2.1.15 + # Test frameworks on the python versions they support, according to pypi registry + # Flask + - framework: FLASK_VERSION=2.2.3 + python-version: 3.6 + + # Django + - framework: DJANGO_VERSION=1.11.29 + python-version: 3.8 + - framework: DJANGO_VERSION=1.11.29 + python-version: 3.9 + - framework: DJANGO_VERSION=1.11.29 + python-version: '3.10' + - framework: DJANGO_VERSION=1.11.29 + python-version: 3.11 + - framework: DJANGO_VERSION=4.0.10 + python-version: 3.6 + - framework: DJANGO_VERSION=4.0.10 + python-version: 3.7 + - framework: DJANGO_VERSION=4.1.7 + python-version: 3.5 + - framework: DJANGO_VERSION=4.1.7 + python-version: 3.6 + - framework: DJANGO_VERSION=4.1.7 + python-version: 3.7 + + # Twisted + - framework: TWISTED_VERSION=20.3.0 + python-version: 3.11 + - framework: TWISTED_VERSION=22.10.0 + python-version: 3.6 - # twisted/treq setup.py allows: - # Twisted < 18.7.0 on python < 3.7 - # Twisted >= 18.7.0 on python >= 3.7 - # So we put twisted < 18.x in the matrix - # and disallow python 3.7 and 3.8 here. - - python-version: 3.7 - framework: TWISTED_VERSION=15.5.0 treq==15.1.0 zope.interface==4.1.3 - - python-version: 3.7 - framework: TWISTED_VERSION=16.1.0 treq==16.12.0 zope.interface==4.1.3 - - python-version: 3.7 - framework: TWISTED_VERSION=16.2.0 treq==16.12.0 zope.interface==4.1.3 - - python-version: 3.7 - framework: TWISTED_VERSION=16.3.0 treq==16.12.0 zope.interface==4.2.0 - - python-version: 3.7 - framework: TWISTED_VERSION=16.4.0 treq==17.8.0 zope.interface==4.2.0 - - python-version: 3.7 - framework: TWISTED_VERSION=16.5.0 treq==17.8.0 zope.interface==4.2.0 - - python-version: 3.7 - framework: TWISTED_VERSION=16.6.0 treq==17.8.0 zope.interface==4.3.0 - - python-version: 3.7 - framework: TWISTED_VERSION=17.1.0 treq==20.4.1 zope.interface==4.3.0 - - python-version: 3.8 - framework: TWISTED_VERSION=15.5.0 treq==15.1.0 zope.interface==4.1.3 - - python-version: 3.8 - framework: TWISTED_VERSION=16.1.0 treq==16.12.0 zope.interface==4.1.3 - - python-version: 3.8 - framework: TWISTED_VERSION=16.2.0 treq==16.12.0 zope.interface==4.1.3 - - python-version: 3.8 - framework: TWISTED_VERSION=16.3.0 treq==16.12.0 zope.interface==4.2.0 - - python-version: 3.8 - framework: TWISTED_VERSION=16.4.0 treq==17.8.0 zope.interface==4.2.0 - - python-version: 3.8 - framework: TWISTED_VERSION=16.5.0 treq==17.8.0 zope.interface==4.3.0 - - python-version: 3.8 - framework: TWISTED_VERSION=16.6.0 treq==17.8.0 zope.interface==4.3.0 - - python-version: 3.8 - framework: TWISTED_VERSION=17.1.0 treq==20.4.1 zope.interface==4.3.0 - - - python-version: 2.7 - framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 2.7 - framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 2.7 - framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.4 - framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.4 - framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.4 - framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.5 - framework: STARLETTE_VERSION=0.12.12 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.5 - framework: STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.5 - framework: STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 - - - python-version: 2.7 - framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 2.7 - framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 2.7 - framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.4 - framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.4 - framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.4 - framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.5 - framework: FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.5 - framework: FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 - - python-version: 3.5 - framework: FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 - include: - - python-version: 2.7 - framework: FLASK_VERSION=0.9 - - python-version: 3.4 - framework: DJANGO_VERSION=1.7.11 - - python-version: 3.4 - framework: DJANGO_VERSION=1.8.19 - - python-version: 3.4 - framework: DJANGO_VERSION=1.9.13 - - python-version: 3.4 - framework: DJANGO_VERSION=1.10.8 - - python-version: 3.5 - framework: DJANGO_VERSION=1.8.19 - - python-version: 3.5 - framework: DJANGO_VERSION=1.9.13 - - python-version: 3.5 - framework: DJANGO_VERSION=1.10.8 - - python-version: 3.7 - framework: TWISTED_VERSION=18.9.0 treq==20.4.1 zope.interface==4.5.0 - - python-version: 3.7 - framework: TWISTED_VERSION=19.10.0 treq==20.4.1 zope.interface==4.6.0 - - python-version: 3.7 - framework: TWISTED_VERSION=20.3.0 treq==20.4.1 zope.interface==4.7.0 - - python-version: 3.8 - framework: TWISTED_VERSION=18.9.0 treq==20.4.1 zope.interface==4.5.0 - - python-version: 3.8 - framework: TWISTED_VERSION=19.10.0 treq==20.4.1 zope.interface==4.6.0 - - python-version: 3.8 - framework: TWISTED_VERSION=20.3.0 treq==20.4.1 zope.interface==4.7.0 steps: - uses: actions/checkout@v2 with: submodules: recursive - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: pip install setuptools==39.2.0 --force-reinstall + - name: Install Python 3.6 dependencies + if: ${{ contains(matrix.python-version, '3.6') }} + # typing-extensions dropped support for Python 3.6 in version 4.2 + run: pip install "typing-extensions<4.2" requests==2.27.0 blinker==1.5 immutables==0.19 + + - name: Install Python 3.7 dependencies + if: ${{ contains(matrix.python-version, '3.7') }} + # immutables dropped support for Python<3.8 in version 0.20 + run: pip install immutables==0.19 - name: Set the framework run: echo ${{ matrix.framework }} >> $GITHUB_ENV @@ -232,27 +109,5 @@ jobs: if: ${{ contains(matrix.framework, 'FASTAPI_VERSION') }} run: pip install fastapi==$FASTAPI_VERSION - - name: Install Python 2 dependencies - if: ${{ contains(matrix.python-version, '2.7') }} - # certifi dropped support for Python 2 in 2020.4.5.2 but only started - # using Python 3 syntax in 2022.5.18. 2021.10.8 is the last release with - # Python 2 support. - run: pip install certifi==2021.10.8 - - - name: Install Python 3.4 dependencies - if: ${{ contains(matrix.python-version, '3.4') }} - # certifi uses the 'typing' from Python 3.5 module starting in 2022.5.18 - run: pip install certifi==2021.10.8 "typing-extensions<4" - - - name: Install Python 3.5 dependencies - if: ${{ contains(matrix.python-version, '3.5') }} - # typing-extensions dropped support for Python 3.5 in version 4 - run: pip install "typing-extensions<4" - - - name: Install Python 3.6 dependencies - if: ${{ contains(matrix.python-version, '3.6') }} - # typing-extensions dropped support for Python 3.6 in version 4.2 - run: pip install "typing-extensions<4.2" - - name: Run tests run: python setup.py test diff --git a/README.md b/README.md index 9d83553c..59006709 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,48 @@ -# Pyrollbar +

+ rollbar-logo +

+ +

Pyrollbar

+ +

+ Proactively discover, predict, and resolve errors in real-time with Rollbar’s error monitoring platform. Start tracking errors today! +

+ ![Build Status](https://github.com/rollbar/pyrollbar/workflows/Pyrollbar%20CI/badge.svg?tag=latest) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rollbar) Python notifier for reporting exceptions, errors, and log messages to [Rollbar](https://rollbar.com). -# Setup Instructions +## Key benefits of using Pyrollbar are: +- **Frameworks:** Pyrollbar supports popular Python frameworks such as Django, Flask, FastAPI, AWS Lambda and more! +- **Automatic error grouping:** Rollbar aggregates Occurrences caused by the same error into Items that represent application issues. Learn more about reducing log noise. +- **Advanced search:** Filter items by many different properties. Learn more about search. +- **Customizable notifications:** Rollbar supports several messaging and incident management tools where your team can get notified about errors and important events by real-time alerts. Learn more about Rollbar notifications. + +## Versions Supported + +| PyRollbar Version | Python Version Compatibility | Support Level | +|-------------------|-----------------------------------------------|---------------------| +| 1.0.0 | 3.6, 3.7. 3.8, 3.9, 3.10, 3.11 | Full | +| 0.16.3 | 2.7, 3.4, 3.5, 3.6, 3.7. 3.8, 3.9, 3.10, 3.11 | Security Fixes Only | + +#### Support Level Definitions + +**Full** - We will support new features of the library and test against all supported versions. + +**Security Fixes Only** - We will only provide critical security fixes for the library. + +## Setup Instructions 1. [Sign up for a Rollbar account](https://rollbar.com/signup) 2. Follow the [Quick Start](https://docs.rollbar.com/docs/python#section-quick-start) instructions in our [Python SDK docs](https://docs.rollbar.com/docs/python) to install pyrollbar and configure it for your platform. -# Usage and Reference +## Usage and Reference For complete usage instructions and configuration reference, see our [Python SDK docs](https://docs.rollbar.com/docs/python). -# Release History & Changelog +## Release History & Changelog See our [Releases](https://github.com/rollbar/pyrollbar/releases) page for a list of all releases, including changes. diff --git a/UPGRADE_FROM_RATCHET.md b/UPGRADE_FROM_RATCHET.md deleted file mode 100644 index 8db9af63..00000000 --- a/UPGRADE_FROM_RATCHET.md +++ /dev/null @@ -1,31 +0,0 @@ -# Upgrading from pyratchet - -Execute: - - $ pip uninstall ratchet - -Then: - - $ pip install rollbar - -## Generic Python or a non-Django/non-Pyramid framework - -Change your initialization call from `ratchet.init(...)` to `rollbar.init(...)`. - -Search your app for all references to `ratchet` and replace them with `rollbar`. - -## Pyratchet running with Django - -In your `settings.py`: -- change `'ratchet.contrib.django.middleware.RatchetNotifierMiddleware'` to `'rollbar.contrib.django.middleware.RollbarNotifierMiddleware'` -- rename your `RATCHET` configuration dict to `ROLLBAR` - -Search your app for all references to `ratchet` and replace them with `rollbar`. - -## Pyratchet running with Pyramid - -In your `ini` file: -- change the include `ratchet.contrib.pyramid` to `rollbar.contrib.pyramid` -- rename your `ratchet.*` configuration variables to `rollbar.*` - -Search your app for all references to `ratchet` and replace them with `rollbar`. \ No newline at end of file diff --git a/default.nix b/default.nix deleted file mode 100644 index 253f06dc..00000000 --- a/default.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - pkgs ? import {}, - python ? pkgs.python36, -}: - -with pkgs; -with python.pkgs; - -buildPythonPackage rec { - name = "pyrollbar"; - src = builtins.filterSource (path: type: - type != "unknown" && - baseNameOf path != ".git" && - baseNameOf path != "result" && - !(pkgs.lib.hasSuffix ".nix" path) - ) ./.; - propagatedBuildInputs = [requests six]; -} - diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 2ec18d2c..42b8d5ec 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -16,23 +16,18 @@ import uuid import wsgiref.util import warnings +import queue +from urllib.parse import parse_qs, urljoin import requests -import six -from rollbar.lib import events, filters, dict_merge, parse_qs, text, transport, urljoin, iteritems, defaultJSONEncode +from rollbar.lib import events, filters, dict_merge, transport, defaultJSONEncode -__version__ = '0.16.3' +__version__ = '1.0.0' __log_name__ = 'rollbar' log = logging.getLogger(__log_name__) -try: - # 2.x - import Queue as queue -except ImportError: - # 3.x - import queue # import request objects from various frameworks, if available try: @@ -60,7 +55,7 @@ del ImproperlyConfigured try: - from werkzeug.wrappers import BaseRequest as WerkzeugRequest + from werkzeug.wrappers import Request as WerkzeugRequest except (ImportError, SyntaxError): WerkzeugRequest = None @@ -86,7 +81,7 @@ try: from google.appengine.api.urlfetch import fetch as AppEngineFetch -except ImportError: +except (ImportError, KeyError): AppEngineFetch = None try: @@ -124,7 +119,7 @@ def wrap(*args, **kwargs): from twisted.internet.ssl import CertificateOptions from twisted.internet import task, defer, ssl, reactor from zope.interface import implementer - + @implementer(IPolicyForHTTPS) class VerifyHTTPS(object): def __init__(self): @@ -275,7 +270,12 @@ def _get_fastapi_request(): 'root': None, # root path to your code 'branch': None, # git branch name 'code_version': None, - 'handler': 'default', # 'blocking', 'thread' (default), 'async', 'agent', 'tornado', 'gae', 'twisted' or 'httpx' + # 'blocking', 'thread' (default), 'async', 'agent', 'tornado', 'gae', 'twisted', 'httpx' or 'thread_pool' + # 'async' requires Python 3.4 or higher. + # 'httpx' requires Python 3.7 or higher. + # 'thread_pool' requires Python 3.2 or higher. + 'handler': 'default', + 'thread_pool_workers': None, 'endpoint': DEFAULT_ENDPOINT, 'timeout': DEFAULT_TIMEOUT, 'agent.log_file': 'log.rollbar', @@ -322,6 +322,7 @@ def _get_fastapi_request(): 'request_pool_connections': None, 'request_pool_maxsize': None, 'request_max_retries': None, + 'batch_transforms': False, } _CURRENT_LAMBDA_CONTEXT = None @@ -336,11 +337,13 @@ def _get_fastapi_request(): from rollbar.lib.transforms.scrub_redact import REDACT_REF from rollbar.lib import transforms +from rollbar.lib import type_info from rollbar.lib.transforms.scrub import ScrubTransform from rollbar.lib.transforms.scruburl import ScrubUrlTransform from rollbar.lib.transforms.scrub_redact import ScrubRedactTransform from rollbar.lib.transforms.serializable import SerializableTransform from rollbar.lib.transforms.shortener import ShortenerTransform +from rollbar.lib.transforms.batched import BatchedTransform ## public api @@ -383,6 +386,9 @@ def init(access_token, environment='production', scrub_fields=None, url_fields=N if SETTINGS.get('handler') == 'agent': agent_log = _create_agent_log() + elif SETTINGS.get('handler') == 'thread_pool': + from rollbar.lib.thread_pool import init_pool + init_pool(SETTINGS.get('thread_pool_workers', None)) if not SETTINGS['locals']['safelisted_types'] and SETTINGS['locals']['whitelisted_types']: warnings.warn('whitelisted_types deprecated use safelisted_types instead', DeprecationWarning) @@ -414,10 +420,11 @@ def init(access_token, environment='production', scrub_fields=None, url_fields=N ] if SETTINGS['locals']['enabled']: - shortener_keys.append(('body', 'trace', 'frames', '*', 'code')) - shortener_keys.append(('body', 'trace', 'frames', '*', 'args', '*')) - shortener_keys.append(('body', 'trace', 'frames', '*', 'kwargs', '*')) - shortener_keys.append(('body', 'trace', 'frames', '*', 'locals', '*')) + for prefix in (('body', 'trace'), ('body', 'trace_chain', '*')): + shortener_keys.append(prefix + ('frames', '*', 'code')) + shortener_keys.append(prefix + ('frames', '*', 'args', '*')) + shortener_keys.append(prefix + ('frames', '*', 'kwargs', '*')) + shortener_keys.append(prefix + ('frames', '*', 'locals', '*')) shortener_keys.extend(SETTINGS['shortener_keys']) @@ -523,6 +530,7 @@ def send_payload(payload, access_token): - 'gae': calls _send_payload_appengine() (which makes a blocking call to Google App Engine) - 'twisted': calls _send_payload_twisted() (which makes an async HTTP request using Twisted and Treq) - 'httpx': calls _send_payload_httpx() (which makes an async HTTP request using HTTPX) + - 'thread_pool': uses a pool of worker threads to make HTTP requests off the main thread. Returns immediately. """ payload = events.on_payload(payload) if payload is False: @@ -569,6 +577,8 @@ def send_payload(payload, access_token): _send_payload_async(payload_str, access_token) elif handler == 'thread': _send_payload_thread(payload_str, access_token) + elif handler == 'thread_pool': + _send_payload_thread_pool(payload_str, access_token) else: # default to 'thread' _send_payload_thread(payload_str, access_token) @@ -676,7 +686,7 @@ def prev_page(self): def _resolve_exception_class(idx, filter): cls, level = filter - if isinstance(cls, six.string_types): + if isinstance(cls, str): # Lazily resolve class name parts = cls.split('.') module = '.'.join(parts[:-1]) @@ -815,7 +825,7 @@ def _trace_data(cls, exc, trace): 'frames': frames, 'exception': { 'class': getattr(cls, '__name__', cls.__class__.__name__), - 'message': text(exc), + 'message': str(exc), } } @@ -892,7 +902,7 @@ def _build_base_data(request, level='error'): 'level': level, 'language': 'python %s' % '.'.join(str(x) for x in sys.version_info[:3]), 'notifier': SETTINGS['notifier'], - 'uuid': text(uuid.uuid4()), + 'uuid': str(uuid.uuid4()), } if SETTINGS.get('code_version'): @@ -947,9 +957,9 @@ def hasuser(request): return True else: retval = {} if getattr(user, 'id', None): - retval['id'] = text(user.id) + retval['id'] = str(user.id) elif getattr(user, 'user_id', None): - retval['id'] = text(user.user_id) + retval['id'] = str(user.user_id) # id is required, so only include username/email if we have an id if retval.get('id'): @@ -966,7 +976,7 @@ def hasuser(request): return True user_id = user_id_prop() if callable(user_id_prop) else user_id_prop if not user_id: return None - return {'id': text(user_id)} + return {'id': str(user_id)} def _get_func_from_frame(frame): @@ -981,16 +991,6 @@ def _get_func_from_frame(frame): return func -def _flatten_nested_lists(l): - ret = [] - for x in l: - if isinstance(x, list): - ret.extend(_flatten_nested_lists(x)) - else: - ret.append(x) - return ret - - def _add_locals_data(trace_data, exc_info): if not SETTINGS['locals']['enabled']: return @@ -1025,15 +1025,7 @@ def _add_locals_data(trace_data, exc_info): # Optionally fill in locals for this frame if arginfo.locals and _check_add_locals(cur_frame, frame_num, num_frames): # Get all of the named args - # - # args can be a nested list of args in the case where there - # are anonymous tuple args provided. - # e.g. in Python 2 you can: - # def func((x, (a, b), z)): - # return x + a + b + z - # - # func((1, (1, 2), 3)) - argspec = _flatten_nested_lists(arginfo.args) + argspec = arginfo.args if arginfo.varargs is not None: varargspec = arginfo.varargs @@ -1063,7 +1055,7 @@ def _add_locals_data(trace_data, exc_info): cur_frame['keywordspec'] = keywordspec if _locals: try: - cur_frame['locals'] = dict((k, _serialize_frame_data(v)) for k, v in iteritems(_locals)) + cur_frame['locals'] = {k: _serialize_frame_data(v) for k, v in _locals.items()} except Exception: log.exception('Error while serializing frame data.') @@ -1071,10 +1063,11 @@ def _add_locals_data(trace_data, exc_info): def _serialize_frame_data(data): - for transform in (ScrubRedactTransform(), _serialize_transform): - data = transforms.transform(data, transform) - - return data + return transforms.transform( + data, + [ScrubRedactTransform(), _serialize_transform], + batch_transforms=SETTINGS['batch_transforms'] + ) def _add_lambda_context_data(data): @@ -1350,7 +1343,7 @@ def _build_wsgi_request_data(request): if 'QUERY_STRING' in request: request_data['GET'] = parse_qs(request['QUERY_STRING'], keep_blank_values=True) # Collapse single item arrays - request_data['GET'] = dict((k, v[0] if len(v) == 1 else v) for k, v in request_data['GET'].items()) + request_data['GET'] = {k: (v[0] if len(v) == 1 else v) for k, v in request_data['GET'].items()} request_data['headers'] = _extract_wsgi_headers(request.items()) @@ -1466,10 +1459,12 @@ def _build_server_data(): def _transform(obj, key=None): - for transform in _transforms: - obj = transforms.transform(obj, transform, key=key) - - return obj + return transforms.transform( + obj, + _transforms, + key=key, + batch_transforms=SETTINGS['batch_transforms'] + ) def _build_payload(data): @@ -1477,7 +1472,7 @@ def _build_payload(data): Returns the full payload as a string. """ - for k, v in iteritems(data): + for k, v in data.items(): data[k] = _transform(v, key=(k,)) payload = { @@ -1510,6 +1505,18 @@ def _send_payload_thread(payload_str, access_token): thread.start() +def _send_payload_pool(payload_str, access_token): + try: + _post_api('item/', payload_str, access_token=access_token) + except Exception as e: + log.exception('Exception while posting item %r', e) + + +def _send_payload_thread_pool(payload_str, access_token): + from rollbar.lib.thread_pool import submit + submit(_send_payload_pool, payload_str, access_token) + + def _send_payload_appengine(payload_str, access_token): try: _post_api_appengine('item/', payload_str, access_token=access_token) diff --git a/rollbar/contrib/django/middleware.py b/rollbar/contrib/django/middleware.py index 5f413447..5ad25fe8 100644 --- a/rollbar/contrib/django/middleware.py +++ b/rollbar/contrib/django/middleware.py @@ -86,7 +86,6 @@ def get_payload_data(self, request, exc): from django.core.exceptions import MiddlewareNotUsed from django.conf import settings from django.http import Http404 -from six import reraise try: from django.urls import resolve @@ -185,6 +184,24 @@ def _should_ignore_404(url): url_patterns = getattr(settings, 'ROLLBAR', {}).get('ignorable_404_urls', ()) return any(p.search(url) for p in url_patterns) +def _apply_sensitive_post_params(request): + sensitive_post_parameters = getattr( + request, "sensitive_post_parameters", [] + ) + if not sensitive_post_parameters: + return + mutable = request.POST._mutable + request.POST._mutable = True + + if sensitive_post_parameters == "__ALL__": + for param in request.POST: + request.POST[param] = "******" + return + + for param in sensitive_post_parameters: + if param in request.POST: + request.POST[param] = "******" + request.POST._mutable = mutable class RollbarNotifierMiddleware(MiddlewareMixin): def __init__(self, get_response=None): @@ -276,6 +293,8 @@ def process_response(self, request, response): def process_exception(self, request, exc): if isinstance(exc, Http404) and _should_ignore_404(request.get_full_path()): return + _apply_sensitive_post_params(request) + rollbar.report_exc_info( sys.exc_info(), request, @@ -301,10 +320,13 @@ def process_response(self, request, response): try: if hasattr(request, '_rollbar_notifier_original_http404_exc_info'): exc_type, exc_value, exc_traceback = request._rollbar_notifier_original_http404_exc_info - reraise(exc_type, exc_value, exc_traceback) + if exc_value is None: + exc_value = Http404() + raise exc_value.with_traceback(exc_traceback) else: raise Http404() except Exception as exc: + _apply_sensitive_post_params(request) rollbar.report_exc_info( sys.exc_info(), request, diff --git a/rollbar/contrib/fastapi/utils.py b/rollbar/contrib/fastapi/utils.py index 74186133..db4902ec 100644 --- a/rollbar/contrib/fastapi/utils.py +++ b/rollbar/contrib/fastapi/utils.py @@ -21,6 +21,33 @@ def __init__(self, version, reason=''): return super().__init__(err_msg) +def is_current_version_higher_or_equal(current_version, min_version): + """ + Compare two version strings and return True if the current version is higher or equal to the minimum version. + + Note: This function only compares the release segment of the version string. + """ + def parse_version(version): + """Parse the release segment of a version string into a list of strings.""" + parsed = [''] + current_segment = 0 + for c in version: + if c.isdigit(): + parsed[current_segment] += c + elif c == '.': + current_segment += 1 + parsed.append('') + else: + break + if parsed[-1] == '': + parsed.pop() + return parsed + + current = tuple(map(int, parse_version(current_version))) + minimum = tuple(map(int, parse_version(min_version))) + return current >= minimum + + class fastapi_min_version: def __init__(self, min_version): self.min_version = min_version @@ -28,7 +55,10 @@ def __init__(self, min_version): def __call__(self, func): @functools.wraps(func) def wrapper(*args, **kwargs): - if fastapi.__version__ < self.min_version: + if not is_current_version_higher_or_equal( + fastapi.__version__, + self.min_version, + ): raise FastAPIVersionError( self.min_version, reason=f'to use {func.__name__}() function' ) diff --git a/rollbar/contrib/pyramid/__init__.py b/rollbar/contrib/pyramid/__init__.py index f02c7a79..e133e15b 100644 --- a/rollbar/contrib/pyramid/__init__.py +++ b/rollbar/contrib/pyramid/__init__.py @@ -138,7 +138,7 @@ def hook(request, data): environment = kw.pop('environment', 'production') if kw.get('scrub_fields'): - kw['scrub_fields'] = set([str.strip(x) for x in kw.get('scrub_fields').split('\n') if x]) + kw['scrub_fields'] = {str.strip(x) for x in kw.get('scrub_fields').split('\n') if x} if kw.get('exception_level_filters'): r = DottedNameResolver() diff --git a/rollbar/examples/flask/app.py b/rollbar/examples/flask/app.py index 440b63ac..c4391540 100644 --- a/rollbar/examples/flask/app.py +++ b/rollbar/examples/flask/app.py @@ -9,8 +9,7 @@ app = Flask(__name__) -@app.before_first_request -def init_rollbar(): +with app.app_context(): rollbar.init('ACCESS_TOKEN', environment='development') # send exceptions from `app` to rollbar, using flask's signal system. got_request_exception.connect(rollbar.contrib.flask.report_exception, app) diff --git a/rollbar/lib/__init__.py b/rollbar/lib/__init__.py index 78a32d87..8eb7be69 100644 --- a/rollbar/lib/__init__.py +++ b/rollbar/lib/__init__.py @@ -1,98 +1,33 @@ import base64 import collections import copy -import os -import sys from array import array -import json -try: - # Python 3 - from collections.abc import Mapping -except ImportError: - # Python 2.7 - from collections import Mapping - -import six -from six.moves import urllib +from collections.abc import Mapping -iteritems = six.iteritems -reprlib = six.moves.reprlib - -binary_type = six.binary_type -integer_types = six.integer_types -number_types = integer_types + (float, ) -string_types = six.string_types +binary_type = bytes +integer_types = int +number_types = (float, int) +string_types = str sequence_types = (Mapping, list, tuple, set, frozenset, array, collections.deque) -urlparse = urllib.parse.urlparse -urlsplit = urllib.parse.urlsplit -urlunparse = urllib.parse.urlunparse -urlunsplit = urllib.parse.urlunsplit -parse_qs = urllib.parse.parse_qs -urlencode = urllib.parse.urlencode -urljoin = urllib.parse.urljoin -quote = urllib.parse.quote - - -_version = sys.version_info - - -def python_major_version(): - return _version[0] - - -if python_major_version() < 3: - def text(val): - if isinstance(val, (str, unicode)): - return val - - conversion_options = [unicode, lambda x: unicode(x, encoding='utf8')] - for option in conversion_options: - try: - return option(val) - except UnicodeDecodeError: - pass - return repr(val) - - _map = map - - def map(*args): - return _map(*args) - - def force_lower(val): +def force_lower(val): + try: + return val.lower() + except: return str(val).lower() -else: - def text(val): - return str(val) - - _map = map - - def map(*args): - return list(_map(*args)) - - def force_lower(val): - try: - return val.lower() - except: - return str(val).lower() - - -def do_for_python_version(two_fn, three_fn, *args, **kw): - if python_major_version() < 3: - return two_fn(*args, **kw) - return three_fn(*args, **kw) - def prefix_match(key, prefixes): if not key: return False for prefix in prefixes: - common_prefix = os.path.commonprefix((prefix, key)) - if common_prefix == prefix: + if len(prefix) > len(key): + continue + + if prefix == key[:len(prefix)]: return True return False @@ -110,23 +45,22 @@ def key_in(key, keys): def key_match(key1, key2): - key1_len = len(key1) - key2_len = len(key2) - if key1_len != key2_len: + if len(key1) != len(key2): return False - z_key = zip(key1, key2) - num_matches = 0 - for p1, p2 in z_key: - if '*' in (p1, p2) or p1 == p2: - num_matches += 1 + for p1, p2 in zip(key1, key2): + if '*' == p1 or '*' == p2: + continue + if p1 == p2: + continue + return False - return num_matches == key1_len + return True def reverse_list_of_lists(l, apply_each_fn=None): apply_each_fn = apply_each_fn or (lambda x: x) - return map(lambda x: list(reversed(map(apply_each_fn, x))), l or []) + return [reversed([apply_each_fn(x) for x in inner]) for inner in l or []] def build_key_matcher(prefixes_or_suffixes, type='prefix', case_sensitive=False): @@ -183,9 +117,9 @@ def dict_merge(a, b, silence_errors=False): else: try: result[k] = copy.deepcopy(v) - except: + except Exception as e: if not silence_errors: - raise six.reraise(*sys.exc_info()) + raise e result[k] = '' % (v,) @@ -193,7 +127,7 @@ def dict_merge(a, b, silence_errors=False): def circular_reference_label(data, ref_key=None): - ref = '.'.join(map(text, ref_key)) + ref = '.'.join([str(x) for x in ref_key]) return '' % (type(data).__name__, ref) diff --git a/rollbar/lib/_async.py b/rollbar/lib/_async.py index b41dc2f7..4c233069 100644 --- a/rollbar/lib/_async.py +++ b/rollbar/lib/_async.py @@ -4,6 +4,7 @@ import logging import sys from unittest import mock +from urllib.parse import urljoin try: import httpx @@ -12,7 +13,7 @@ import rollbar from rollbar import DEFAULT_TIMEOUT -from rollbar.lib import transport, urljoin +from rollbar.lib import transport log = logging.getLogger(__name__) @@ -138,7 +139,7 @@ async def _post_api_httpx(path, payload_str, access_token=None): ) as client: resp = await client.post( url, - data=payload_str, + content=payload_str, headers=headers, timeout=rollbar.SETTINGS.get('timeout', DEFAULT_TIMEOUT), ) diff --git a/rollbar/lib/thread_pool.py b/rollbar/lib/thread_pool.py new file mode 100644 index 00000000..8bd7162c --- /dev/null +++ b/rollbar/lib/thread_pool.py @@ -0,0 +1,38 @@ +import logging +import os +import sys +from concurrent.futures import ThreadPoolExecutor + +_pool = None # type: ThreadPoolExecutor|None + +log = logging.getLogger(__name__) + + +def init_pool(max_workers): + """ + Creates the thread pool with the max workers. + + :type max_workers: int|None + :param max_workers: If max_workers is None it will use the logic from the standard library to calculate the number + of threads. However, we ported the logic from Python 3.5 to earlier versions. + """ + if max_workers is None and sys.version_info < (3, 5): + max_workers = (os.cpu_count() or 1) * 5 + + global _pool + _pool = ThreadPoolExecutor(max_workers) + + +def submit(worker, payload_str, access_token): + """ + Submit a new task to the thread pool. + + :type worker: function + :type payload_str: str + :type access_token: str + """ + global _pool + if _pool is None: + log.warning('pyrollbar: Thead pool not initialized. Please ensure init_pool() is called prior to submit().') + return + _pool.submit(worker, payload_str, access_token) diff --git a/rollbar/lib/transform.py b/rollbar/lib/transform.py new file mode 100644 index 00000000..2ba7a030 --- /dev/null +++ b/rollbar/lib/transform.py @@ -0,0 +1,36 @@ +class Transform(object): + def default(self, o, key=None): + return o + + def transform_circular_reference(self, o, key=None, ref_key=None): + # By default, we just perform a no-op for circular references. + # Subclasses should implement this method to return whatever representation + # for the circular reference they need. + return self.default(o, key=key) + + def transform_tuple(self, o, key=None): + return self.default(o, key=key) + + def transform_namedtuple(self, o, key=None): + return self.default(o, key=key) + + def transform_list(self, o, key=None): + return self.default(o, key=key) + + def transform_dict(self, o, key=None): + return self.default(o, key=key) + + def transform_number(self, o, key=None): + return self.default(o, key=key) + + def transform_bytes(self, o, key=None): + return self.default(o, key=key) + + def transform_unicode(self, o, key=None): + return self.default(o, key=key) + + def transform_boolean(self, o, key=None): + return self.default(o, key=key) + + def transform_custom(self, o, key=None): + return self.default(o, key=key) diff --git a/rollbar/lib/transforms/__init__.py b/rollbar/lib/transforms/__init__.py index 3b995055..3f1b8802 100644 --- a/rollbar/lib/transforms/__init__.py +++ b/rollbar/lib/transforms/__init__.py @@ -1,6 +1,15 @@ +from collections.abc import Iterable + from rollbar.lib import ( - python_major_version, binary_type, string_types, integer_types, - number_types, traverse) + binary_type, + string_types, + number_types, + traverse, +) +# NOTE: Don't remove this import, it would cause a breaking change to the library's API. +# The `Transform` class was moved out of this file to prevent a cyclical dependency issue. +from rollbar.lib.transform import Transform +from rollbar.lib.transforms.batched import BatchedTransform _ALLOWED_CIRCULAR_REFERENCE_TYPES = [binary_type, bool, type(None)] @@ -17,72 +26,37 @@ _ALLOWED_CIRCULAR_REFERENCE_TYPES = tuple(_ALLOWED_CIRCULAR_REFERENCE_TYPES) -class Transform(object): - def default(self, o, key=None): - return o - - def transform_circular_reference(self, o, key=None, ref_key=None): - # By default, we just perform a no-op for circular references. - # Subclasses should implement this method to return whatever representation - # for the circular reference they need. - return self.default(o, key=key) - - def transform_tuple(self, o, key=None): - return self.default(o, key=key) - - def transform_namedtuple(self, o, key=None): - return self.default(o, key=key) - - def transform_list(self, o, key=None): - return self.default(o, key=key) - - def transform_dict(self, o, key=None): - return self.default(o, key=key) - - def transform_number(self, o, key=None): - return self.default(o, key=key) - - def transform_py2_str(self, o, key=None): - return self.default(o, key=key) - - def transform_py3_bytes(self, o, key=None): - return self.default(o, key=key) +def transform(obj, transforms, key=None, batch_transforms=False): + if isinstance(transforms, Transform): + transforms = [transforms] - def transform_unicode(self, o, key=None): - return self.default(o, key=key) + if batch_transforms: + transforms = [BatchedTransform(transforms)] - def transform_boolean(self, o, key=None): - return self.default(o, key=key) + for transform in transforms: + obj = _transform(obj, transform, key=key) - def transform_custom(self, o, key=None): - return self.default(o, key=key) + return obj -def transform(obj, transform, key=None): +def _transform(obj, transform, key=None): key = key or () def do_transform(type_name, val, key=None, **kw): - fn = getattr(transform, 'transform_%s' % type_name, transform.transform_custom) + fn = getattr(transform, "transform_%s" % type_name, transform.transform_custom) val = fn(val, key=key, **kw) return val - if python_major_version() < 3: - def string_handler(s, key=None): - if isinstance(s, str): - return do_transform('py2_str', s, key=key) - elif isinstance(s, unicode): - return do_transform('unicode', s, key=key) - else: - def string_handler(s, key=None): - if isinstance(s, bytes): - return do_transform('py3_bytes', s, key=key) - elif isinstance(s, str): - return do_transform('unicode', s, key=key) + def string_handler(s, key=None): + if isinstance(s, bytes): + return do_transform("bytes", s, key=key) + elif isinstance(s, str): + return do_transform("unicode", s, key=key) def default_handler(o, key=None): if isinstance(o, bool): - return do_transform('boolean', o, key=key) + return do_transform("boolean", o, key=key) # There is a quirk in the current version (1.1.6) of the enum # backport enum34 which causes it to not have the same @@ -90,26 +64,29 @@ def default_handler(o, key=None): # they are instances of numbers but not number types. if isinstance(o, number_types): if type(o) not in number_types: - return do_transform('custom', o, key=key) + return do_transform("custom", o, key=key) else: - return do_transform('number', o, key=key) + return do_transform("number", o, key=key) - return do_transform('custom', o, key=key) + return do_transform("custom", o, key=key) handlers = { - 'string_handler': string_handler, - 'tuple_handler': lambda o, key=None: do_transform('tuple', o, key=key), - 'namedtuple_handler': lambda o, key=None: do_transform('namedtuple', o, key=key), - 'list_handler': lambda o, key=None: do_transform('list', o, key=key), - 'set_handler': lambda o, key=None: do_transform('set', o, key=key), - 'mapping_handler': lambda o, key=None: do_transform('dict', o, key=key), - 'circular_reference_handler': lambda o, key=None, ref_key=None: - do_transform('circular_reference', o, key=key, ref_key=ref_key), - 'default_handler': default_handler, - 'allowed_circular_reference_types': _ALLOWED_CIRCULAR_REFERENCE_TYPES + "string_handler": string_handler, + "tuple_handler": lambda o, key=None: do_transform("tuple", o, key=key), + "namedtuple_handler": lambda o, key=None: do_transform( + "namedtuple", o, key=key + ), + "list_handler": lambda o, key=None: do_transform("list", o, key=key), + "set_handler": lambda o, key=None: do_transform("set", o, key=key), + "mapping_handler": lambda o, key=None: do_transform("dict", o, key=key), + "circular_reference_handler": lambda o, key=None, ref_key=None: do_transform( + "circular_reference", o, key=key, ref_key=ref_key + ), + "default_handler": default_handler, + "allowed_circular_reference_types": _ALLOWED_CIRCULAR_REFERENCE_TYPES, } return traverse.traverse(obj, key=key, **handlers) -__all__ = ['transform', 'Transform'] +__all__ = ["transform", "Transform"] diff --git a/rollbar/lib/transforms/batched.py b/rollbar/lib/transforms/batched.py new file mode 100644 index 00000000..b0d5d04d --- /dev/null +++ b/rollbar/lib/transforms/batched.py @@ -0,0 +1,77 @@ +from rollbar.lib.transform import Transform +from rollbar.lib import ( + number_types, + type_info, +) + + +def do_transform(transform, type_name, val, key=None, **kw): + fn = getattr(transform, "transform_%s" % type_name, transform.transform_custom) + val = fn(val, key=key, **kw) + + return val + + +def string_handler(transform, s, key=None): + if isinstance(s, bytes): + return do_transform(transform, "bytes", s, key=key) + elif isinstance(s, str): + return do_transform(transform, "unicode", s, key=key) + + +def default_handler(transform, o, key=None): + if isinstance(o, bool): + return do_transform(transform, "boolean", o, key=key) + + # There is a quirk in the current version (1.1.6) of the enum + # backport enum34 which causes it to not have the same + # behavior as Python 3.4+. One way to identify IntEnums is that + # they are instances of numbers but not number types. + if isinstance(o, number_types): + if type(o) not in number_types: + return do_transform(transform, "custom", o, key=key) + else: + return do_transform(transform, "number", o, key=key) + + return do_transform(transform, "custom", o, key=key) + + +handlers = { + type_info.STRING: string_handler, + type_info.TUPLE: lambda transform, o, key=None: do_transform( + transform, "tuple", o, key=key + ), + type_info.NAMEDTUPLE: lambda transform, o, key=None: do_transform( + transform, "namedtuple", o, key=key + ), + type_info.LIST: lambda transform, o, key=None: do_transform( + transform, "list", o, key=key + ), + type_info.SET: lambda transform, o, key=None: do_transform( + transform, "set", o, key=key + ), + type_info.MAPPING: lambda transform, o, key=None: do_transform( + transform, "dict", o, key=key + ), + type_info.CIRCULAR: lambda transform, o, key=None, ref_key=None: do_transform( + transform, "circular_reference", o, key=key, ref_key=ref_key + ), + type_info.DEFAULT: default_handler, +} + + +class BatchedTransform(Transform): + def __init__(self, transforms): + super(BatchedTransform, self).__init__() + self._transforms = transforms + + def default(self, o, key=None): + for transform in self._transforms: + node_type = type_info.get_type(o) + handler = handlers.get(node_type, handlers.get(type_info.DEFAULT)) + o = handler(transform, o, key=key) + + return o + + +__all__ = ["BatchedTransform"] diff --git a/rollbar/lib/transforms/scrub.py b/rollbar/lib/transforms/scrub.py index 8032f648..ff0af810 100644 --- a/rollbar/lib/transforms/scrub.py +++ b/rollbar/lib/transforms/scrub.py @@ -1,17 +1,21 @@ import random -from rollbar.lib import build_key_matcher, text -from rollbar.lib.transforms import Transform +from rollbar.lib import build_key_matcher +from rollbar.lib.transform import Transform class ScrubTransform(Transform): + suffix_matcher = None def __init__(self, suffixes=None, redact_char='*', randomize_len=True): super(ScrubTransform, self).__init__() - self.suffix_matcher = build_key_matcher(suffixes, type='suffix') + if suffixes is not None and len(suffixes) > 0: + self.suffix_matcher = build_key_matcher(suffixes, type='suffix') self.redact_char = redact_char self.randomize_len = randomize_len def in_scrub_fields(self, key): + if self.suffix_matcher is None: + return False return self.suffix_matcher(key) def redact(self, val): @@ -21,7 +25,7 @@ def redact(self, val): try: _len = len(val) except: - _len = len(text(val)) + _len = len(str(val)) return self.redact_char * _len diff --git a/rollbar/lib/transforms/scruburl.py b/rollbar/lib/transforms/scruburl.py index 7b570462..0ab922cc 100644 --- a/rollbar/lib/transforms/scruburl.py +++ b/rollbar/lib/transforms/scruburl.py @@ -1,6 +1,7 @@ import re +from urllib.parse import urlsplit, urlencode, urlunsplit, parse_qs -from rollbar.lib import iteritems, map, urlsplit, urlencode, urlunsplit, parse_qs, string_types, binary_type +from rollbar.lib import string_types, binary_type from rollbar.lib.transforms.scrub import ScrubTransform @@ -21,7 +22,7 @@ def __init__(self, randomize_len=randomize_len) self.scrub_username = scrub_username self.scrub_password = scrub_password - self.params_to_scrub = set(map(lambda x: x.lower(), params_to_scrub)) + self.params_to_scrub = {x.lower() for x in params_to_scrub or []} def in_scrub_fields(self, key): # Returning True here because we want to scrub URLs out of @@ -51,9 +52,9 @@ def redact(self, url_string): if not netloc: return url_string - for qs_param, vals in iteritems(qs_params): + for qs_param, vals in qs_params.items(): if qs_param.lower() in self.params_to_scrub: - vals2 = map(_redact, vals) + vals2 = [_redact(x) for x in vals] qs_params[qs_param] = vals2 scrubbed_qs = urlencode(qs_params, doseq=True) diff --git a/rollbar/lib/transforms/serializable.py b/rollbar/lib/transforms/serializable.py index 20102896..49f95d29 100644 --- a/rollbar/lib/transforms/serializable.py +++ b/rollbar/lib/transforms/serializable.py @@ -4,9 +4,8 @@ from rollbar.lib import ( circular_reference_label, float_infinity_label, float_nan_label, undecodable_object_label, unencodable_object_label) -from rollbar.lib import iteritems, python_major_version, text -from rollbar.lib.transforms import Transform +from rollbar.lib.transform import Transform class SerializableTransform(Transform): @@ -25,7 +24,7 @@ def transform_namedtuple(self, o, key=None): for field in tuple_dict: new_vals.append(transformed_dict[field]) - return '<%s>' % text(o._make(new_vals)) + return '<%s>' % str(o._make(new_vals)) def transform_number(self, o, key=None): if math.isnan(o): @@ -35,15 +34,7 @@ def transform_number(self, o, key=None): else: return o - def transform_py2_str(self, o, key=None): - try: - o.decode('utf8') - except UnicodeDecodeError: - return undecodable_object_label(o) - else: - return o - - def transform_py3_bytes(self, o, key=None): + def transform_bytes(self, o, key=None): try: o.decode('utf8') except UnicodeDecodeError: @@ -61,20 +52,14 @@ def transform_unicode(self, o, key=None): def transform_dict(self, o, key=None): ret = {} - for k, v in iteritems(o): + for k, v in o.items(): if isinstance(k, string_types) or isinstance(k, binary_type): - if python_major_version() < 3: - if isinstance(k, unicode): - new_k = self.transform_unicode(k) - else: - new_k = self.transform_py2_str(k) + if isinstance(k, bytes): + new_k = self.transform_bytes(k) else: - if isinstance(k, bytes): - new_k = self.transform_py3_bytes(k) - else: - new_k = self.transform_unicode(k) + new_k = self.transform_unicode(k) else: - new_k = text(k) + new_k = str(k) ret[new_k] = v diff --git a/rollbar/lib/transforms/shortener.py b/rollbar/lib/transforms/shortener.py index 68574d9d..f0392912 100644 --- a/rollbar/lib/transforms/shortener.py +++ b/rollbar/lib/transforms/shortener.py @@ -1,18 +1,14 @@ from array import array import collections import itertools +import reprlib -try: - # Python 3 - from collections.abc import Mapping -except ImportError: - # Python 2.7 - from collections import Mapping +from collections.abc import Mapping from rollbar.lib import ( - integer_types, iteritems, key_in, number_types, reprlib, sequence_types, - string_types, text) -from rollbar.lib.transforms import Transform + integer_types, key_in, number_types, sequence_types, + string_types) +from rollbar.lib.transform import Transform _type_name_mapping = { @@ -36,11 +32,11 @@ def __init__(self, safe_repr=True, keys=None, **sizes): self.keys = keys self._repr = reprlib.Repr() - for name, size in iteritems(sizes): + for name, size in sizes.items(): setattr(self._repr, name, size) def _get_max_size(self, obj): - for name, _type in iteritems(_type_name_mapping): + for name, _type in _type_name_mapping.items(): # Special case for dicts since we are using collections.abc.Mapping # to provide better type checking for dict-like objects if name == 'mapping': @@ -66,7 +62,7 @@ def _shorten_mapping(self, obj, max_keys): return {k: obj[k] for k in itertools.islice(obj.keys(), max_keys)} def _shorten_basic(self, obj, max_len): - val = text(obj) + val = str(obj) if len(val) <= max_len: return obj @@ -77,7 +73,7 @@ def _shorten_other(self, obj): return None if self.safe_repr: - obj = text(obj) + obj = str(obj) return self._repr.repr(obj) diff --git a/rollbar/lib/traverse.py b/rollbar/lib/traverse.py index dcfd0fc0..6a613dad 100644 --- a/rollbar/lib/traverse.py +++ b/rollbar/lib/traverse.py @@ -1,30 +1,29 @@ import logging -try: - # Python 3 - from collections.abc import Mapping - from collections.abc import Sequence -except ImportError: - # Python 2.7 - from collections import Mapping - from collections import Sequence - -from rollbar.lib import binary_type, iteritems, string_types, circular_reference_label - -CIRCULAR = -1 -DEFAULT = 0 -MAPPING = 1 -TUPLE = 2 -NAMEDTUPLE = 3 -LIST = 4 -SET = 5 -STRING = 6 + +from rollbar.lib import binary_type, string_types, circular_reference_label + +# NOTE: Don't remove this line of code as it would cause a breaking change +# to the library's API. The items imported here were originally in this file +# but were moved to a new file for easier use elsewhere. +from rollbar.lib.type_info import ( + get_type, + CIRCULAR, + DEFAULT, + MAPPING, + TUPLE, + NAMEDTUPLE, + LIST, + SET, + STRING, +) + log = logging.getLogger(__name__) def _noop_circular(a, **kw): - return circular_reference_label(a, ref_key=kw.get('ref_key')) + return circular_reference_label(a, ref_key=kw.get("ref_key")) def _noop(a, **_): @@ -63,64 +62,45 @@ def _noop_mapping(a, **_): } -def get_type(obj): - if isinstance(obj, (string_types, binary_type)): - return STRING - - if isinstance(obj, Mapping): - return MAPPING - - if isinstance(obj, tuple): - if hasattr(obj, '_fields'): - return NAMEDTUPLE - - return TUPLE - - if isinstance(obj, set): - return SET - - if isinstance(obj, Sequence): - return LIST - - return DEFAULT - - -def traverse(obj, - key=(), - string_handler=_default_handlers[STRING], - tuple_handler=_default_handlers[TUPLE], - namedtuple_handler=_default_handlers[NAMEDTUPLE], - list_handler=_default_handlers[LIST], - set_handler=_default_handlers[SET], - mapping_handler=_default_handlers[MAPPING], - default_handler=_default_handlers[DEFAULT], - circular_reference_handler=_default_handlers[CIRCULAR], - allowed_circular_reference_types=None, - memo=None, - **custom_handlers): - +def traverse( + obj, + key=(), + string_handler=_default_handlers[STRING], + tuple_handler=_default_handlers[TUPLE], + namedtuple_handler=_default_handlers[NAMEDTUPLE], + list_handler=_default_handlers[LIST], + set_handler=_default_handlers[SET], + mapping_handler=_default_handlers[MAPPING], + default_handler=_default_handlers[DEFAULT], + circular_reference_handler=_default_handlers[CIRCULAR], + allowed_circular_reference_types=None, + memo=None, + **custom_handlers +): memo = memo or {} obj_id = id(obj) obj_type = get_type(obj) ref_key = memo.get(obj_id) if ref_key: - if not allowed_circular_reference_types or not isinstance(obj, allowed_circular_reference_types): + if not allowed_circular_reference_types or not isinstance( + obj, allowed_circular_reference_types + ): return circular_reference_handler(obj, key=key, ref_key=ref_key) memo[obj_id] = key kw = { - 'string_handler': string_handler, - 'tuple_handler': tuple_handler, - 'namedtuple_handler': namedtuple_handler, - 'list_handler': list_handler, - 'set_handler': set_handler, - 'mapping_handler': mapping_handler, - 'default_handler': default_handler, - 'circular_reference_handler': circular_reference_handler, - 'allowed_circular_reference_types': allowed_circular_reference_types, - 'memo': memo + "string_handler": string_handler, + "tuple_handler": tuple_handler, + "namedtuple_handler": namedtuple_handler, + "list_handler": list_handler, + "set_handler": set_handler, + "mapping_handler": mapping_handler, + "default_handler": default_handler, + "circular_reference_handler": circular_reference_handler, + "allowed_circular_reference_types": allowed_circular_reference_types, + "memo": memo, } kw.update(custom_handlers) @@ -128,25 +108,48 @@ def traverse(obj, if obj_type is STRING: return string_handler(obj, key=key) elif obj_type is TUPLE: - return tuple_handler(tuple(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) + return tuple_handler( + tuple( + traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj) + ), + key=key, + ) elif obj_type is NAMEDTUPLE: - return namedtuple_handler(obj._make(traverse(v, key=key + (k,), **kw) for k, v in iteritems(obj._asdict())), key=key) + return namedtuple_handler( + obj._make( + traverse(v, key=key + (k,), **kw) + for k, v in obj._asdict().items() + ), + key=key, + ) elif obj_type is LIST: - return list_handler(list(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) + return list_handler( + [traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)], + key=key, + ) elif obj_type is SET: - return set_handler(set(traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)), key=key) + return set_handler( + {traverse(elem, key=key + (i,), **kw) for i, elem in enumerate(obj)}, + key=key, + ) elif obj_type is MAPPING: - return mapping_handler(dict((k, traverse(v, key=key + (k,), **kw)) for k, v in iteritems(obj)), key=key) + return mapping_handler( + {k: traverse(v, key=key + (k,), **kw) for k, v in obj.items()}, + key=key, + ) elif obj_type is DEFAULT: - for handler_type, handler in iteritems(custom_handlers): + for handler_type, handler in custom_handlers.items(): if isinstance(obj, handler_type): return handler(obj, key=key) except: # use the default handler for unknown object types - log.debug("Exception while traversing object using type-specific " - "handler. Switching to default handler.", exc_info=True) + log.debug( + "Exception while traversing object using type-specific " + "handler. Switching to default handler.", + exc_info=True, + ) return default_handler(obj, key=key) -__all__ = ['traverse'] +__all__ = ["traverse"] diff --git a/rollbar/lib/type_info.py b/rollbar/lib/type_info.py new file mode 100644 index 00000000..7cf0af54 --- /dev/null +++ b/rollbar/lib/type_info.py @@ -0,0 +1,49 @@ +from rollbar.lib import binary_type, string_types + + +from collections.abc import Mapping, Sequence, Set + + +CIRCULAR = -1 +DEFAULT = 0 +MAPPING = 1 +TUPLE = 2 +NAMEDTUPLE = 3 +LIST = 4 +SET = 5 +STRING = 6 + + +def get_type(obj): + if isinstance(obj, (string_types, binary_type)): + return STRING + + if isinstance(obj, Mapping): + return MAPPING + + if isinstance(obj, tuple): + if hasattr(obj, "_fields"): + return NAMEDTUPLE + + return TUPLE + + if isinstance(obj, set): + return SET + + if isinstance(obj, Sequence): + return LIST + + return DEFAULT + + +__all__ = [ + "CIRCULAR", + "DEFAULT", + "MAPPING", + "TUPLE", + "NAMEDTUPLE", + "LIST", + "SET", + "STRING", + "get_type", +] diff --git a/rollbar/test/__init__.py b/rollbar/test/__init__.py index 2a8cdfda..5f52f1bf 100644 --- a/rollbar/test/__init__.py +++ b/rollbar/test/__init__.py @@ -1,13 +1,15 @@ -import unittest2 +import unittest SNOWMAN = b'\xe2\x98\x83' SNOWMAN_UNICODE = SNOWMAN.decode('utf8') -class BaseTest(unittest2.TestCase): +class BaseTest(unittest.TestCase): pass def discover(): - return unittest2.defaultTestLoader.discover(__name__) + loader = unittest.TestLoader() + suite = loader.discover(__name__) + return suite diff --git a/rollbar/test/asgi_tests/__init__.py b/rollbar/test/asgi_tests/__init__.py index f937df26..6f51d3b1 100644 --- a/rollbar/test/asgi_tests/__init__.py +++ b/rollbar/test/asgi_tests/__init__.py @@ -1,9 +1,9 @@ import sys -import unittest2 +import unittest def _load_tests(loader, tests, pattern): - return unittest2.TestSuite() + return unittest.TestSuite() if sys.version_info < (3, 5): diff --git a/rollbar/test/asgi_tests/test_integration.py b/rollbar/test/asgi_tests/test_integration.py index 84578094..23312aed 100644 --- a/rollbar/test/asgi_tests/test_integration.py +++ b/rollbar/test/asgi_tests/test_integration.py @@ -1,6 +1,12 @@ +import unittest +import sys + from rollbar.test import BaseTest +ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 5) + +@unittest.skipUnless(ALLOWED_PYTHON_VERSION, 'ASGI implementation requires Python3.5+') class IntegrationTest(BaseTest): def test_should_integrate_if__integrate_defined(self): from rollbar.contrib.asgi.integration import IntegrationBase diff --git a/rollbar/test/asgi_tests/test_middleware.py b/rollbar/test/asgi_tests/test_middleware.py index bbafe3c2..d949d596 100644 --- a/rollbar/test/asgi_tests/test_middleware.py +++ b/rollbar/test/asgi_tests/test_middleware.py @@ -2,12 +2,9 @@ import importlib import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock -import unittest2 +import unittest import rollbar from rollbar.lib._async import AsyncMock @@ -17,7 +14,7 @@ ASYNC_REPORT_ENABLED = sys.version_info >= (3, 6) -@unittest2.skipUnless(ALLOWED_PYTHON_VERSION, 'ASGI implementation requires Python3.5+') +@unittest.skipUnless(ALLOWED_PYTHON_VERSION, 'ASGI implementation requires Python3.5+') class ReporterMiddlewareTest(BaseTest): default_settings = copy.deepcopy(rollbar.SETTINGS) @@ -62,7 +59,7 @@ def test_should_add_framework_name_to_payload(self, mock_send_payload, *mocks): self.assertIn('asgi', payload['data']['framework']) - @unittest2.skipUnless(ASYNC_REPORT_ENABLED, 'Requires Python 3.6+') + @unittest.skipUnless(ASYNC_REPORT_ENABLED, 'Requires Python 3.6+') @mock.patch('rollbar.lib._async.report_exc_info', new_callable=AsyncMock) @mock.patch('rollbar.report_exc_info') def test_should_use_async_report_exc_info_if_default_handler( @@ -81,7 +78,7 @@ def test_should_use_async_report_exc_info_if_default_handler( self.assertTrue(async_report_exc_info.called) self.assertFalse(sync_report_exc_info.called) - @unittest2.skipUnless(ASYNC_REPORT_ENABLED, 'Requires Python 3.6+') + @unittest.skipUnless(ASYNC_REPORT_ENABLED, 'Requires Python 3.6+') @mock.patch('rollbar.lib._async.report_exc_info', new_callable=AsyncMock) @mock.patch('rollbar.report_exc_info') def test_should_use_async_report_exc_info_if_any_async_handler( @@ -100,7 +97,7 @@ def test_should_use_async_report_exc_info_if_any_async_handler( self.assertTrue(async_report_exc_info.called) self.assertFalse(sync_report_exc_info.called) - @unittest2.skipUnless(ASYNC_REPORT_ENABLED, 'Requires Python 3.6+') + @unittest.skipUnless(ASYNC_REPORT_ENABLED, 'Requires Python 3.6+') @mock.patch('logging.Logger.warning') @mock.patch('rollbar.lib._async.report_exc_info', new_callable=AsyncMock) @mock.patch('rollbar.report_exc_info') diff --git a/rollbar/test/asgi_tests/test_spec.py b/rollbar/test/asgi_tests/test_spec.py index cec8f42a..82cd17c8 100644 --- a/rollbar/test/asgi_tests/test_spec.py +++ b/rollbar/test/asgi_tests/test_spec.py @@ -1,14 +1,14 @@ import inspect import sys -import unittest2 +import unittest from rollbar.test import BaseTest ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 5) -@unittest2.skipUnless(ALLOWED_PYTHON_VERSION, 'ASGI implementation requires Python3.5+') +@unittest.skipUnless(ALLOWED_PYTHON_VERSION, 'ASGI implementation requires Python3.5+') class ASGISpecTest(BaseTest): def test_asgi_v3_middleware_is_single_callable_coroutine(self): from rollbar.contrib.asgi import ReporterMiddleware diff --git a/rollbar/test/async_tests/__init__.py b/rollbar/test/async_tests/__init__.py index b461bd75..3fd86539 100644 --- a/rollbar/test/async_tests/__init__.py +++ b/rollbar/test/async_tests/__init__.py @@ -1,9 +1,9 @@ import sys -import unittest2 +import unittest def _load_tests(loader, tests, pattern): - return unittest2.TestSuite() + return unittest.TestSuite() if sys.version_info < (3, 6): diff --git a/rollbar/test/async_tests/test_async.py b/rollbar/test/async_tests/test_async.py index 7c5e1abf..6843c8ec 100644 --- a/rollbar/test/async_tests/test_async.py +++ b/rollbar/test/async_tests/test_async.py @@ -1,12 +1,9 @@ import copy import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock -import unittest2 +import unittest import rollbar from rollbar.lib._async import AsyncMock @@ -15,7 +12,7 @@ ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless(ALLOWED_PYTHON_VERSION, 'Async support requires Python3.6+') +@unittest.skipUnless(ALLOWED_PYTHON_VERSION, 'Async support requires Python3.6+') class AsyncLibTest(BaseTest): default_settings = copy.deepcopy(rollbar.SETTINGS) diff --git a/rollbar/test/fastapi_tests/__init__.py b/rollbar/test/fastapi_tests/__init__.py index b461bd75..3fd86539 100644 --- a/rollbar/test/fastapi_tests/__init__.py +++ b/rollbar/test/fastapi_tests/__init__.py @@ -1,9 +1,9 @@ import sys -import unittest2 +import unittest def _load_tests(loader, tests, pattern): - return unittest2.TestSuite() + return unittest.TestSuite() if sys.version_info < (3, 6): diff --git a/rollbar/test/fastapi_tests/test_logger.py b/rollbar/test/fastapi_tests/test_logger.py index d9ca3754..49a4a8a6 100644 --- a/rollbar/test/fastapi_tests/test_logger.py +++ b/rollbar/test/fastapi_tests/test_logger.py @@ -1,10 +1,7 @@ import importlib import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock try: import fastapi @@ -13,7 +10,7 @@ except ImportError: FASTAPI_INSTALLED = False -import unittest2 +import unittest import rollbar from rollbar.test import BaseTest @@ -21,7 +18,7 @@ ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless( +@unittest.skipUnless( FASTAPI_INSTALLED and ALLOWED_PYTHON_VERSION, 'FastAPI LoggerMiddleware requires Python3.6+', ) diff --git a/rollbar/test/fastapi_tests/test_middleware.py b/rollbar/test/fastapi_tests/test_middleware.py index 26c929eb..c49336e4 100644 --- a/rollbar/test/fastapi_tests/test_middleware.py +++ b/rollbar/test/fastapi_tests/test_middleware.py @@ -2,10 +2,7 @@ import importlib import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock try: import fastapi @@ -14,7 +11,7 @@ except ImportError: FASTAPI_INSTALLED = False -import unittest2 +import unittest import rollbar from rollbar.lib._async import AsyncMock @@ -23,7 +20,7 @@ ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless( +@unittest.skipUnless( FASTAPI_INSTALLED and ALLOWED_PYTHON_VERSION, 'FastAPI requires Python3.6+' ) class ReporterMiddlewareTest(BaseTest): @@ -258,7 +255,7 @@ async def root(): 'Failed to report asynchronously. Trying to report synchronously.' ) - @unittest2.skipUnless( + @unittest.skipUnless( sys.version_info >= (3, 6), 'Global request access requires Python 3.6+' ) @mock.patch('rollbar.contrib.starlette.middleware.store_current_request') @@ -305,7 +302,7 @@ async def read_root(): scope = store_current_request.call_args[0][0] self.assertDictContainsSubset(expected_scope, scope) - @unittest2.skipUnless( + @unittest.skipUnless( sys.version_info >= (3, 6), 'Global request access is supported in Python 3.6+' ) def test_should_return_current_request(self): diff --git a/rollbar/test/fastapi_tests/test_routing.py b/rollbar/test/fastapi_tests/test_routing.py index 28fb0467..c26ca159 100644 --- a/rollbar/test/fastapi_tests/test_routing.py +++ b/rollbar/test/fastapi_tests/test_routing.py @@ -3,10 +3,7 @@ import json import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock try: import fastapi @@ -17,7 +14,7 @@ FASTAPI_INSTALLED = False ALLOWED_FASTAPI_VERSION = False -import unittest2 +import unittest import rollbar from rollbar.lib._async import AsyncMock @@ -27,7 +24,7 @@ ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless( +@unittest.skipUnless( FASTAPI_INSTALLED and ALLOWED_PYTHON_VERSION, 'FastAPI requires Python3.6+' ) class LoggingRouteUnsupportedFastAPIVersionTest(BaseTest): @@ -64,10 +61,10 @@ def test_should_disable_loading_route_handler_if_fastapi_is_too_old(self): fastapi.__version__ = fastapi_version -@unittest2.skipUnless( +@unittest.skipUnless( FASTAPI_INSTALLED and ALLOWED_PYTHON_VERSION, 'FastAPI requires Python3.6+' ) -@unittest2.skipUnless(ALLOWED_FASTAPI_VERSION, 'FastAPI v0.41.0+ is required') +@unittest.skipUnless(ALLOWED_FASTAPI_VERSION, 'FastAPI v0.41.0+ is required') class LoggingRouteTest(BaseTest): default_settings = copy.deepcopy(rollbar.SETTINGS) @@ -686,7 +683,7 @@ def test_should_warn_if_middleware_in_use(self): ' This can cause in duplicate occurrences.' ) - @unittest2.skipUnless( + @unittest.skipUnless( sys.version_info >= (3, 6), 'Global request access requires Python 3.6+' ) @mock.patch('rollbar.contrib.fastapi.routing.store_current_request') @@ -733,7 +730,7 @@ async def read_root(): scope = store_current_request.call_args[0][0] self.assertDictContainsSubset(expected_scope, scope) - @unittest2.skipUnless( + @unittest.skipUnless( sys.version_info >= (3, 6), 'Global request access is supported in Python 3.6+' ) def test_should_return_current_request(self): diff --git a/rollbar/test/fastapi_tests/test_utils.py b/rollbar/test/fastapi_tests/test_utils.py index 9a787372..549e9b8c 100644 --- a/rollbar/test/fastapi_tests/test_utils.py +++ b/rollbar/test/fastapi_tests/test_utils.py @@ -7,14 +7,14 @@ except ImportError: FASTAPI_INSTALLED = False -import unittest2 +import unittest from rollbar.test import BaseTest ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless( +@unittest.skipUnless( FASTAPI_INSTALLED and ALLOWED_PYTHON_VERSION, 'FastAPI requires Python3.6+' ) class UtilsMiddlewareTest(BaseTest): @@ -66,7 +66,7 @@ def test_should_return_empty_list_if_rollbar_middlewares_not_installed(self): self.assertListEqual(middlewares, []) -@unittest2.skipUnless( +@unittest.skipUnless( FASTAPI_INSTALLED and ALLOWED_PYTHON_VERSION, 'FastAPI requires Python3.6+' ) class UtilsBareRoutingTest(BaseTest): @@ -121,3 +121,35 @@ async def read_root(): app.include_router(router) self.assertFalse(has_bare_routing(app)) + +@unittest.skipUnless( + FASTAPI_INSTALLED and ALLOWED_PYTHON_VERSION, 'FastAPI requires Python3.6+' +) +class UtilsVersionCompareTest(BaseTest): + def test_is_current_version_higher_or_equal(self): + # Copied from https://semver.org/#spec-item-11 + versions = [ + '1.0.0-alpha', + '1.0.0-alpha.1', + '1.0.0-alpha.beta', + '1.0.0-beta', + '1.0.0-beta.2', + '1.0.0-beta.11', + '1.0.0-rc.1', + '1.0.0', + '1.1.1', + '1.100.0-beta2', + '1.100.0-beta3', + ] + + from rollbar.contrib.fastapi.utils import is_current_version_higher_or_equal + + previous_version = None + for version in versions: + print(f'{version} >= {previous_version}') + if previous_version is None: + previous_version = version + continue + with self.subTest(f'{version} >= {previous_version}'): + self.assertTrue(is_current_version_higher_or_equal(version, previous_version)) + previous_version = version diff --git a/rollbar/test/flask_tests/test_flask.py b/rollbar/test/flask_tests/test_flask.py index f8b020e3..b97227bb 100644 --- a/rollbar/test/flask_tests/test_flask.py +++ b/rollbar/test/flask_tests/test_flask.py @@ -6,10 +6,7 @@ import sys import os -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock import rollbar diff --git a/rollbar/test/starlette_tests/__init__.py b/rollbar/test/starlette_tests/__init__.py index b461bd75..3fd86539 100644 --- a/rollbar/test/starlette_tests/__init__.py +++ b/rollbar/test/starlette_tests/__init__.py @@ -1,9 +1,9 @@ import sys -import unittest2 +import unittest def _load_tests(loader, tests, pattern): - return unittest2.TestSuite() + return unittest.TestSuite() if sys.version_info < (3, 6): diff --git a/rollbar/test/starlette_tests/test_logger.py b/rollbar/test/starlette_tests/test_logger.py index ed9acb44..3ed51e68 100644 --- a/rollbar/test/starlette_tests/test_logger.py +++ b/rollbar/test/starlette_tests/test_logger.py @@ -1,10 +1,7 @@ import importlib import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock try: import starlette @@ -13,7 +10,7 @@ except ImportError: STARLETTE_INSTALLED = False -import unittest2 +import unittest import rollbar from rollbar.test import BaseTest @@ -21,7 +18,7 @@ ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless( +@unittest.skipUnless( STARLETTE_INSTALLED and ALLOWED_PYTHON_VERSION, 'Starlette LoggerMiddleware requires Python3.6+', ) diff --git a/rollbar/test/starlette_tests/test_middleware.py b/rollbar/test/starlette_tests/test_middleware.py index 1500f487..7c9f6554 100644 --- a/rollbar/test/starlette_tests/test_middleware.py +++ b/rollbar/test/starlette_tests/test_middleware.py @@ -2,10 +2,7 @@ import importlib import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock try: import starlette @@ -14,7 +11,7 @@ except ImportError: STARLETTE_INSTALLED = False -import unittest2 +import unittest import rollbar from rollbar.lib._async import AsyncMock @@ -23,7 +20,7 @@ ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless( +@unittest.skipUnless( STARLETTE_INSTALLED and ALLOWED_PYTHON_VERSION, 'Starlette requires Python3.6+' ) class ReporterMiddlewareTest(BaseTest): @@ -232,7 +229,7 @@ async def root(request): 'Failed to report asynchronously. Trying to report synchronously.' ) - @unittest2.skipUnless( + @unittest.skipUnless( sys.version_info >= (3, 6), 'Global request access requires Python 3.6+' ) @mock.patch('rollbar.contrib.starlette.middleware.store_current_request') @@ -276,7 +273,7 @@ async def root(request): scope = store_current_request.call_args[0][0] self.assertDictContainsSubset(expected_scope, scope) - @unittest2.skipUnless( + @unittest.skipUnless( sys.version_info >= (3, 6), 'Global request access is supported in Python 3.6+' ) def test_should_return_current_request(self): diff --git a/rollbar/test/starlette_tests/test_requests.py b/rollbar/test/starlette_tests/test_requests.py index 34aabebe..75bacb1d 100644 --- a/rollbar/test/starlette_tests/test_requests.py +++ b/rollbar/test/starlette_tests/test_requests.py @@ -7,14 +7,14 @@ except ImportError: STARLETTE_INSTALLED = False -import unittest2 +import unittest from rollbar.test import BaseTest ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) -@unittest2.skipUnless( +@unittest.skipUnless( STARLETTE_INSTALLED and ALLOWED_PYTHON_VERSION, 'Global request access requires Python3.6+', ) diff --git a/rollbar/test/test_batched_transform.py b/rollbar/test/test_batched_transform.py new file mode 100644 index 00000000..96c7d04c --- /dev/null +++ b/rollbar/test/test_batched_transform.py @@ -0,0 +1,59 @@ +from rollbar.lib.transforms import transform +from rollbar.lib.transform import Transform +from rollbar.lib.traverse import traverse + +from rollbar.test import BaseTest + + +class TrackingTransformer(Transform): + def __init__(self): + self.got = [] + + def default(self, o, key=None): + self.got.append((o, key)) + return o + + +class BatchedTransformTest(BaseTest): + def assertTrackingTransform(self, input): + tracking_transformer = TrackingTransformer() + + transforms = [ + tracking_transformer, + tracking_transformer, + ] + + transform(input, transforms, batch_transforms=True) + + want = [] + + def dup_watch_handler(o, key=None): + want.append((o, key)) + want.append((o, key)) + return o + + traverse( + input, + string_handler=dup_watch_handler, + tuple_handler=dup_watch_handler, + namedtuple_handler=dup_watch_handler, + list_handler=dup_watch_handler, + set_handler=dup_watch_handler, + mapping_handler=dup_watch_handler, + default_handler=dup_watch_handler, + circular_reference_handler=dup_watch_handler, + ) + + self.assertEqual(want, tracking_transformer.got) + + def test_number(self): + self.assertTrackingTransform(1) + + def test_flat_list(self): + self.assertTrackingTransform([0, 1, 2, 3]) + + def test_flat_tuple(self): + self.assertTrackingTransform((0, 1, 2, 3)) + + def test_nested_object(self): + self.assertTrackingTransform((0, [1, 2], {"a": 3, "b": (4, 5)})) diff --git a/rollbar/test/test_lib.py b/rollbar/test/test_lib.py index 2793fab8..7d792638 100644 --- a/rollbar/test/test_lib.py +++ b/rollbar/test/test_lib.py @@ -1,7 +1,8 @@ -from rollbar.lib import dict_merge +from rollbar.lib import dict_merge, prefix_match from rollbar.test import BaseTest + class RollbarLibTest(BaseTest): def test_dict_merge_not_dict(self): a = {'a': {'b': 42}} @@ -10,6 +11,14 @@ def test_dict_merge_not_dict(self): self.assertEqual(99, result) + def test_prefix_match(self): + key = ['password', 'argspec', '0'] + self.assertTrue(prefix_match(key, [['password']])) + + def test_prefix_match(self): + key = ['environ', 'argspec', '0'] + self.assertFalse(prefix_match(key, [['password']])) + def test_dict_merge_dicts_independent(self): a = {'a': {'b': 42}} b = {'x': {'y': 99}} diff --git a/rollbar/test/test_loghandler.py b/rollbar/test/test_loghandler.py index 5c3cf1c2..c4c80d32 100644 --- a/rollbar/test/test_loghandler.py +++ b/rollbar/test/test_loghandler.py @@ -6,10 +6,7 @@ import logging import sys -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock import rollbar from rollbar.logger import RollbarHandler @@ -81,25 +78,23 @@ def test_request_is_get_from_log_record_if_present(self): logger.warning("Warning message", extra={"request": request}) self.assertEqual(report_message_mock.call_args[1]["request"], request) - # Python 2.6 doesnt support extra param in logger.exception. - if not sys.version_info[:2] == (2, 6): - # if you call logger.exception outside of an exception - # handler, it shouldn't try to report exc_info, since it - # won't have any - with mock.patch("rollbar.report_exc_info") as report_exc_info: - with mock.patch("rollbar.report_message") as report_message_mock: + # if you call logger.exception outside of an exception + # handler, it shouldn't try to report exc_info, since it + # won't have any + with mock.patch("rollbar.report_exc_info") as report_exc_info: + with mock.patch("rollbar.report_message") as report_message_mock: + logger.exception("Exception message", extra={"request": request}) + report_exc_info.assert_not_called() + self.assertEqual(report_message_mock.call_args[1]["request"], request) + + with mock.patch("rollbar.report_exc_info") as report_exc_info: + with mock.patch("rollbar.report_message") as report_message_mock: + try: + raise Exception() + except: logger.exception("Exception message", extra={"request": request}) - report_exc_info.assert_not_called() - self.assertEqual(report_message_mock.call_args[1]["request"], request) - - with mock.patch("rollbar.report_exc_info") as report_exc_info: - with mock.patch("rollbar.report_message") as report_message_mock: - try: - raise Exception() - except: - logger.exception("Exception message", extra={"request": request}) - self.assertEqual(report_exc_info.call_args[1]["request"], request) - report_message_mock.assert_not_called() + self.assertEqual(report_exc_info.call_args[1]["request"], request) + report_message_mock.assert_not_called() @mock.patch('rollbar.send_payload') def test_nested_exception_trace_chain(self, send_payload): diff --git a/rollbar/test/test_pyramid.py b/rollbar/test/test_pyramid.py index 4f643bbd..63c4a3ca 100644 --- a/rollbar/test/test_pyramid.py +++ b/rollbar/test/test_pyramid.py @@ -1,7 +1,4 @@ -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock from rollbar.test import BaseTest diff --git a/rollbar/test/test_rollbar.py b/rollbar/test/test_rollbar.py index ed797883..3107e9e4 100644 --- a/rollbar/test/test_rollbar.py +++ b/rollbar/test/test_rollbar.py @@ -11,15 +11,13 @@ from StringIO import StringIO except ImportError: from io import StringIO -try: - from unittest import mock -except ImportError: - import mock + +from unittest import mock import unittest import rollbar -from rollbar.lib import python_major_version, string_types +from rollbar.lib import string_types from rollbar.test import BaseTest @@ -463,18 +461,8 @@ def test_get_request_fastapi_middleware(self): app = FastAPI() app.add_middleware(ReporterMiddleware) - # Inject annotations and decorate endpoint dynamically - # to avoid SyntaxError for older Python - # - # This is the code we'd use if we had not loaded the test file on Python 2. - # - # @app.get('/{param}') - # def root(param, fastapi_request: Request): - # current_request = rollbar.get_request() - # - # self.assertEqual(current_request, fastapi_request) - - def root(param, fastapi_request): + @app.get('/{param}') + def root(param, fastapi_request: Request): current_request = rollbar.get_request() self.assertEqual(current_request, fastapi_request) @@ -500,18 +488,8 @@ def test_get_request_fastapi_logger(self): app = FastAPI() app.add_middleware(ReporterMiddleware) - # Inject annotations and decorate endpoint dynamically - # to avoid SyntaxError for older Python - # - # This is the code we'd use if we had not loaded the test file on Python 2. - # - # @app.get('/{param}') - # def root(fastapi_request: Request): - # current_request = rollbar.get_request() - # - # self.assertEqual(current_request, fastapi_request) - - def root(param, fastapi_request): + @app.get('/{param}') + def root(fastapi_request: Request): current_request = rollbar.get_request() self.assertEqual(current_request, fastapi_request) @@ -541,18 +519,8 @@ def test_get_request_fastapi_router(self): app = FastAPI() rollbar_add_to(app) - # Inject annotations and decorate endpoint dynamically - # to avoid SyntaxError for older Python - # - # This is the code we'd use if we had not loaded the test file on Python 2. - # - # @app.get('/{param}') - # def root(fastapi_request: Request): - # current_request = rollbar.get_request() - # - # self.assertEqual(current_request, fastapi_request) - - def root(param, fastapi_request): + @app.get('/{param}') + def root(fastapi_request: Request): current_request = rollbar.get_request() self.assertEqual(current_request, fastapi_request) @@ -1035,6 +1003,32 @@ def _raise(): send_payload_httpx.assert_called_once() + @unittest.skipUnless(sys.version_info >= (3, 6), 'assert_called_once support requires Python3.6+') + @mock.patch('rollbar._send_payload_thread_pool') + def test_thread_pool_handler(self, send_payload_thread_pool): + def _raise(): + try: + raise Exception('foo') + except: + rollbar.report_exc_info() + rollbar.SETTINGS['handler'] = 'thread_pool' + _raise() + + send_payload_thread_pool.assert_called_once() + + @unittest.skipUnless(sys.version_info >= (3, 2), 'concurrent.futures support requires Python3.2+') + def test_thread_pool_submit(self): + from rollbar.lib.thread_pool import init_pool, submit + init_pool(1) + ran = {'nope': True} # dict used so it is not shadowed in run + + def run(payload_str, access_token): + ran['nope'] = False + + submit(run, 'foo', 'bar') + self.assertFalse(ran['nope']) + + @mock.patch('rollbar.send_payload') def test_args_constructor(self, send_payload): @@ -1446,8 +1440,7 @@ def _raise(): self.assertRegex(payload['data']['body']['trace']['frames'][-1]['locals']['Password'], r'\*+') self.assertIn('_invalid', payload['data']['body']['trace']['frames'][-1]['locals']) - binary_type_name = 'str' if python_major_version() < 3 else 'bytes' - undecodable_message = '' % (binary_type_name, base64.b64encode(invalid).decode('ascii')) + undecodable_message = '' % ('bytes', base64.b64encode(invalid).decode('ascii')) self.assertEqual(undecodable_message, payload['data']['body']['trace']['frames'][-1]['locals']['_invalid']) @mock.patch('rollbar.send_payload') diff --git a/rollbar/test/test_scrub_redact_transform.py b/rollbar/test/test_scrub_redact_transform.py index 34e3356d..da6abe4b 100644 --- a/rollbar/test/test_scrub_redact_transform.py +++ b/rollbar/test/test_scrub_redact_transform.py @@ -1,11 +1,6 @@ -try: - # Python 3 - from collections.abc import Mapping -except ImportError: - # Python 2.7 - from collections import Mapping +from collections.abc import Mapping -from rollbar.lib import text, transforms +from rollbar.lib import transforms from rollbar.lib.transforms.scrub_redact import ScrubRedactTransform, REDACT_REF from rollbar.test import BaseTest @@ -19,7 +14,7 @@ class NotRedactRef(): try: SCRUBBED = '*' * len(REDACT_REF) except: - SCRUBBED = '*' * len(text(REDACT_REF)) + SCRUBBED = '*' * len(str(REDACT_REF)) class ScrubRedactTransformTest(BaseTest): diff --git a/rollbar/test/test_scrub_transform.py b/rollbar/test/test_scrub_transform.py index 7cc50eb1..be74b61b 100644 --- a/rollbar/test/test_scrub_transform.py +++ b/rollbar/test/test_scrub_transform.py @@ -1,11 +1,6 @@ import copy -try: - # Python 3 - from collections.abc import Mapping -except ImportError: - # Python 2.7 - from collections import Mapping +from collections.abc import Mapping from rollbar.lib import transforms from rollbar.lib.transforms.scrub import ScrubTransform diff --git a/rollbar/test/test_scruburl_transform.py b/rollbar/test/test_scruburl_transform.py index cabeabe6..03a0013c 100644 --- a/rollbar/test/test_scruburl_transform.py +++ b/rollbar/test/test_scruburl_transform.py @@ -1,14 +1,12 @@ -import copy +from urllib.parse import urlparse, parse_qs -from rollbar.lib import map, transforms, string_types, urlparse, parse_qs, python_major_version +from rollbar.lib import transforms, string_types from rollbar.lib.transforms.scruburl import ScrubUrlTransform, _starts_with_auth_re -from rollbar.test import BaseTest, SNOWMAN, SNOWMAN_UNICODE +from rollbar.test import BaseTest, SNOWMAN_UNICODE -if python_major_version() >= 3: - SNOWMAN = SNOWMAN_UNICODE -SNOWMAN_LEN = len(SNOWMAN) +SNOWMAN_LEN = len(SNOWMAN_UNICODE) class ScrubUrlTransformTest(BaseTest): @@ -42,17 +40,17 @@ def _compare_urls(self, url1, url2): if _starts_with_auth_re.match(url2): url2 = '//%s' % url2 - parsed_urls = map(urlparse, (url1, url2)) - qs_params = map(lambda x: parse_qs(x.query, keep_blank_values=True), parsed_urls) - num_params = map(len, qs_params) - param_names = map(lambda x: set(x.keys()), qs_params) + parsed_urls = [urlparse(url) for url in (url1, url2)] + qs_params = [parse_qs(x.query, keep_blank_values=True) for x in parsed_urls] + num_params = [len(x) for x in qs_params] + param_names = [set(x.keys()) for x in qs_params] self.assertEqual(*num_params) self.assertDictEqual(*qs_params) self.assertSetEqual(*param_names) for facet in ('scheme', 'netloc', 'path', 'params', 'username', 'password', 'hostname', 'port'): - comp = map(lambda x: getattr(x, facet), parsed_urls) + comp = [getattr(x, facet) for x in parsed_urls] self.assertEqual(*comp) def test_no_scrub(self): @@ -71,14 +69,14 @@ def test_scrub_simple_url_params(self): self._assertScrubbed(['password'], obj, expected) def test_scrub_utf8_url_params(self): - obj = 'http://foo.com/asdf?password=%s' % SNOWMAN - expected = obj.replace(SNOWMAN, '-' * SNOWMAN_LEN) + obj = 'http://foo.com/asdf?password=%s' % SNOWMAN_UNICODE + expected = obj.replace(SNOWMAN_UNICODE, '-' * SNOWMAN_LEN) self._assertScrubbed(['password'], obj, expected) def test_scrub_utf8_url_keys(self): - obj = 'http://foo.com/asdf?%s=secret' % SNOWMAN + obj = 'http://foo.com/asdf?%s=secret' % SNOWMAN_UNICODE expected = obj.replace('secret', '------') - self._assertScrubbed([str(SNOWMAN)], obj, expected) + self._assertScrubbed([str(SNOWMAN_UNICODE)], obj, expected) def test_scrub_multi_url_params(self): obj = 'http://foo.com/asdf?password=secret&password=secret2&token=TOK&clear=text' diff --git a/rollbar/test/test_serializable_transform.py b/rollbar/test/test_serializable_transform.py index 4ae96248..6978d7c0 100644 --- a/rollbar/test/test_serializable_transform.py +++ b/rollbar/test/test_serializable_transform.py @@ -2,23 +2,16 @@ import base64 import copy import enum +import sys -try: - # Python 3 - from collections.abc import Mapping -except ImportError: - # Python 2.7 - from collections import Mapping +from collections.abc import Mapping -from rollbar.lib import transforms, python_major_version +from rollbar.lib import transforms from rollbar.lib.transforms.serializable import SerializableTransform -from rollbar.test import BaseTest, SNOWMAN, SNOWMAN_UNICODE +from rollbar.test import BaseTest, SNOWMAN_UNICODE -if python_major_version() >= 3: - SNOWMAN = SNOWMAN_UNICODE - -SNOWMAN_LEN = len(SNOWMAN) +SNOWMAN_LEN = len(SNOWMAN_UNICODE) # This base64 encoded string contains bytes that do not @@ -26,8 +19,7 @@ invalid_b64 = b'CuX2JKuXuLVtJ6l1s7DeeQ==' invalid = base64.b64decode(invalid_b64) -binary_type_name = 'str' if python_major_version() < 3 else 'bytes' -undecodable_repr = '' % (binary_type_name, invalid_b64.decode('ascii')) +undecodable_repr = f'' class SerializableTransformTest(BaseTest): @@ -145,7 +137,13 @@ def test_encode_int(self): def test_encode_empty_tuple(self): start = () expected = () - self._assertSerialized(start, expected) + + skip_id_check = False + # different behavior in 3.11 + if sys.version_info >= (3, 11): + skip_id_check = True + + self._assertSerialized(start, expected, skip_id_check=skip_id_check) def test_encode_empty_list(self): start = [] @@ -162,10 +160,7 @@ def test_encode_namedtuple(self): nt = MyType(field_1='this is field 1', field_2=invalid) start = nt - if python_major_version() < 3: - expected = "" % undecodable_repr - else: - expected = "" % undecodable_repr + expected = "" % undecodable_repr self._assertSerialized(start, expected) @@ -234,10 +229,7 @@ def __repr__(self): serializable = SerializableTransform(safelist_types=[CustomRepr]) result = transforms.transform(start, serializable) - if python_major_version() < 3: - self.assertEqual(result['custom'], b'hello') - else: - self.assertRegex(result['custom'], "") + self.assertRegex(result['custom'], "") def test_encode_with_custom_repr_returns_object(self): class CustomRepr(object): @@ -253,11 +245,11 @@ def __repr__(self): def test_encode_with_custom_repr_returns_unicode(self): class CustomRepr(object): def __repr__(self): - return SNOWMAN + return SNOWMAN_UNICODE start = {'hello': 'world', 'custom': CustomRepr()} expected = copy.deepcopy(start) - expected['custom'] = SNOWMAN + expected['custom'] = SNOWMAN_UNICODE self._assertSerialized(start, expected, safelist=[CustomRepr]) def test_encode_with_bad_repr_doesnt_die(self): diff --git a/rollbar/test/test_shortener_transform.py b/rollbar/test/test_shortener_transform.py index 20736f94..55180c34 100644 --- a/rollbar/test/test_shortener_transform.py +++ b/rollbar/test/test_shortener_transform.py @@ -2,11 +2,10 @@ from array import array from collections import deque -import six from rollbar import DEFAULT_LOCALS_SIZES from rollbar.lib import transforms from rollbar.lib.transforms.shortener import ShortenerTransform -from rollbar.lib.traverse import Sequence +from rollbar.lib.type_info import Sequence from rollbar.test import BaseTest @@ -71,9 +70,7 @@ def test_shorten_string(self): self._assert_shortened('string', expected) def test_shorten_long(self): - expected = '179556827339164684...002504519623752387L' - if six.PY3: - expected = '179556827339164684...5002504519623752387' + expected = '179556827339164684...5002504519623752387' self._assert_shortened('long', expected) def test_shorten_mapping(self): @@ -103,6 +100,8 @@ def test_shorten_frozenset(self): def test_shorten_array(self): expected = 'array(\'l\', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...])' + if sys.version_info >= (3, 10): + expected = '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]' self._assert_shortened('array', expected) def test_shorten_deque(self): @@ -112,9 +111,7 @@ def test_shorten_deque(self): self._assert_shortened('deque', expected) def test_shorten_other(self): - expected = '= "3.6"', + 'httpx', 'aiocontextvars; python_version == "3.6"' ] @@ -47,14 +44,14 @@ url='http://github.com/rollbar/pyrollbar', classifiers=[ "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", @@ -76,16 +73,7 @@ "Topic :: System :: Monitoring", ], install_requires=[ - # The currently used version of `setuptools` has a bug, - # so the version requirements are not properly respected. - # - # In the current version, `requests>= 0.12.1` - # always installs the latest version of the package. - 'requests>=0.12.1; python_version == "2.7"', - 'requests>=0.12.1; python_version >= "3.6"', - 'requests<2.26,>=0.12.1; python_version == "3.5"', - 'requests<2.22,>=0.12.1; python_version == "3.4"', - 'six>=1.9.0' + 'requests>=0.12.1', ], tests_require=tests_require, ) diff --git a/shell.nix b/shell.nix deleted file mode 100644 index edeea083..00000000 --- a/shell.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - pkgs ? import {} -}: - -with pkgs; -let -python = let - packageOverrides = self: super: { - pandas = super.pandas.overridePythonAttrs(old: { - doCheck = false; - }); - - twine = super.twine.overridePythonAttrs(old: { - doCheck = false; - }); - - tqdm = super.tqdm.overridePythonAttrs(old: { - doCheck = false; - }); - }; -in python36.override { inherit packageOverrides; }; -pyrollbar = pkgs.callPackage ./. { inherit python; }; -pyenv = python.withPackages(ps: with ps; [ pyrollbar twine unittest2 mock pyramid ]); - -in - -stdenv.mkDerivation { - name = "pyrollbar-shell"; - buildInputs = [ pyenv ]; -} -