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
+
+
+
+
+Pyrollbar
+
+
+ Proactively discover, predict, and resolve errors in real-time with Rollbar’s error monitoring platform. Start tracking errors today!
+
+


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 ];
-}
-