diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 0000000..f94d993 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,34 @@ +name: Tests and Checks +on: + push: + branches: [master] + pull_request: + workflow_dispatch: +jobs: + build: + name: Run checks & tests + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"] + runs-on: ${{ matrix.os }} + # Unreleased CPython breaks for reasons outside our control, so it warns + # instead of failing the build. + continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }} + steps: + - uses: actions/checkout@v4 + - name: Run ruff + uses: astral-sh/ruff-action@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[testing] + - name: Run tests + run: | + pytest -v diff --git a/.gitignore b/.gitignore index e71461f..afad6ed 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist/* *.egg-info/* .noseids .idea +.pi-subagents/ diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 54edb5b..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -global-exclude *.pyc -recursive-include backlash/statics * -include LICENSE diff --git a/backlash/__init__.py b/backlash/__init__.py index c5e4fb0..91ec1fb 100644 --- a/backlash/__init__.py +++ b/backlash/__init__.py @@ -1,5 +1,13 @@ -from backlash.debug import DebuggedApplication -from backlash.wsgi import WsgiDebuggedApplication from backlash.asgi import AsgiDebuggedApplication +from backlash.debug import DebuggedApplication from backlash.tracing.errors import TraceErrorsMiddleware -from backlash.tracing.slowrequests import TraceSlowRequestsMiddleware \ No newline at end of file +from backlash.tracing.slowrequests import TraceSlowRequestsMiddleware +from backlash.wsgi import WsgiDebuggedApplication + +__all__ = [ + 'DebuggedApplication', + 'WsgiDebuggedApplication', + 'AsgiDebuggedApplication', + 'TraceErrorsMiddleware', + 'TraceSlowRequestsMiddleware', +] diff --git a/backlash/_compat.py b/backlash/_compat.py deleted file mode 100644 index 159ce06..0000000 --- a/backlash/_compat.py +++ /dev/null @@ -1,78 +0,0 @@ -import sys -import types - -# True if we are running on Python 3. -PY3 = sys.version_info[0] == 3 -PY2 = sys.version_info[0] == 2 - -if PY3: # pragma: no cover - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - long = int -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - long = long - -def text_(s, encoding='utf-8', errors='strict'): - if isinstance(s, binary_type): - return s.decode(encoding, errors) - return s # pragma: no cover - -def bytes_(s, encoding='utf-8', errors='strict'): - if isinstance(s, text_type): - return s.encode(encoding, errors) - return s - -if PY3: # pragma: no cover - def native_(s, encoding='latin-1', errors='strict'): - if isinstance(s, text_type): - return s - return str(s, encoding, errors) -else: - def native_(s, encoding='latin-1', errors='strict'): - if isinstance(s, text_type): # pragma: no cover - return s.encode(encoding, errors) - return str(s) - -if PY3: # pragma: no cover - def iteritems_(d): - return d.items() -else: - def iteritems_(d): - return d.iteritems() - -if PY3: # pragma: no cover - import builtins - exec_ = getattr(builtins, "exec") - def reraise(exc_info): - etype, exc, tb = exc_info - if exc.__traceback__ is not tb: - raise exc.with_traceback(tb) - raise exc -else: # pragma: no cover - def exec_(code, globs=None, locs=None): - """Execute code in a namespace.""" - if globs is None: - frame = sys._getframe(1) - globs = frame.f_globals - if locs is None: - locs = frame.f_locals - del frame - elif locs is None: - locs = globs - exec("""exec code in globs, locs""") - exec_("""def reraise(exc_info): - raise exc_info[0], exc_info[1], exc_info[2] -""") - -try: - from urllib2 import urlopen -except ImportError: - from urllib.request import urlopen diff --git a/backlash/asgi.py b/backlash/asgi.py index bb53124..8fcbb21 100644 --- a/backlash/asgi.py +++ b/backlash/asgi.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ASGI shell of the interactive traceback debugger.""" import sys from urllib.parse import parse_qs diff --git a/backlash/console.py b/backlash/console.py index e0f9ff9..9e3bb6d 100644 --- a/backlash/console.py +++ b/backlash/console.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ werkzeug.debug.console ~~~~~~~~~~~~~~~~~~~~~~ @@ -8,18 +7,17 @@ :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. :license: BSD. """ -import sys import code -from types import CodeType +import sys import threading +from types import CodeType -from backlash._compat import exec_, text_, binary_type -from backlash.utils import escape from backlash.repr import debug_repr, dump, helper +from backlash.utils import escape _local = threading.local() -class HTMLStringO(object): +class HTMLStringO: """A StringO version that HTML escapes on write.""" def __init__(self): @@ -50,8 +48,8 @@ def reset(self): return val def _write(self, x): - if isinstance(x, binary_type): - x = text_(x, 'utf-8', 'replace') + if isinstance(x, bytes): + x = x.decode('utf-8', 'replace') self._buffer.append(x) def write(self, x): @@ -61,23 +59,24 @@ def writelines(self, x): self._write(escape(''.join(x))) -class ThreadedStream(object): +class ThreadedStream: """Thread-local wrapper for sys.stdout for the interactive console.""" + @staticmethod def push(): if not isinstance(sys.stdout, ThreadedStream): sys.stdout = ThreadedStream() _local.stream = HTMLStringO() - push = staticmethod(push) + @staticmethod def fetch(): try: stream = _local.stream except AttributeError: return '' return stream.reset() - fetch = staticmethod(fetch) + @staticmethod def displayhook(obj): try: stream = _local.stream @@ -87,10 +86,9 @@ def displayhook(obj): # already generating HTML for us. if obj is not None: stream._write(debug_repr(obj)) - displayhook = staticmethod(displayhook) def __setattr__(self, name, value): - raise AttributeError('read only attribute %s' % name) + raise AttributeError(f'read only attribute {name}') def __dir__(self): return dir(sys.__stdout__) @@ -113,7 +111,7 @@ def __repr__(self): sys.displayhook = ThreadedStream.displayhook -class _ConsoleLoader(object): +class _ConsoleLoader: def __init__(self): self._storage = {} @@ -173,7 +171,7 @@ def runsource(self, source): def runcode(self, code): try: - exec_(code, self.globals, self.locals) + exec(code, self.globals, self.locals) except Exception: self.showtraceback() @@ -191,7 +189,7 @@ def write(self, data): sys.stdout.write(data) -class Console(object): +class Console: """An interactive console.""" def __init__(self, globals=None, locals=None, context=None): @@ -204,7 +202,7 @@ def __init__(self, globals=None, locals=None, context=None): def eval(self, code): return self._ipy.runsource(code) -class _ConsoleFrame(object): +class _ConsoleFrame: """Helper class so that we can reuse the frame console code for the standalone console. """ diff --git a/backlash/debug.py b/backlash/debug.py index 1da79f6..4fc8846 100644 --- a/backlash/debug.py +++ b/backlash/debug.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Kept for backward compatibility only: the WSGI debugger historically lived here as ``backlash.debug.DebuggedApplication``. @@ -6,3 +5,5 @@ :class:`backlash.asgi.AsgiDebuggedApplication`. """ from backlash.wsgi import WsgiDebuggedApplication as DebuggedApplication + +__all__ = ['DebuggedApplication'] diff --git a/backlash/debugger.py b/backlash/debugger.py index d1b5cd0..b0de6fe 100644 --- a/backlash/debugger.py +++ b/backlash/debugger.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Transport-agnostic core of the interactive traceback debugger. ``DebuggerCore`` owns the debugger state (captured tracebacks, per-frame @@ -10,18 +9,18 @@ adapt request parsing, response sending, and exception capture to their transport. """ -import mimetypes import logging -from os.path import join, dirname, basename, isfile +import mimetypes +from os.path import basename, dirname, isfile, join -from backlash.tbtools import get_current_traceback, render_console_html from backlash.console import Console +from backlash.tbtools import get_current_traceback, render_console_html from backlash.utils import gen_salt log = logging.getLogger('backlash') -class DebuggerCore(object): +class DebuggerCore: """State and endpoints shared by the WSGI and ASGI debugger middlewares. :param app: the wrapped application (WSGI or ASGI, owned by the shell). @@ -124,7 +123,7 @@ def get_resource(self, filename): return 404, 'text/plain', b'Not Found' -class _ConsoleFrame(object): +class _ConsoleFrame: """Helper class so that we can reuse the frame console code for the standalone console. """ diff --git a/backlash/frtools.py b/backlash/frtools.py index a3f0d35..043bd14 100644 --- a/backlash/frtools.py +++ b/backlash/frtools.py @@ -1,5 +1,7 @@ -import sys, inspect -from .tbtools import Traceback, Frame +import inspect +import sys + +from .tbtools import Frame, Traceback class DumpThread(Exception): diff --git a/backlash/repr.py b/backlash/repr.py index a2e7597..87be620 100644 --- a/backlash/repr.py +++ b/backlash/repr.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ werkzeug.debug.repr ~~~~~~~~~~~~~~~~~~~ @@ -13,15 +12,15 @@ :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. :license: BSD. """ -import sys import re +import sys from traceback import format_exception_only + try: from collections import deque except ImportError: # pragma: no cover deque = None -from backlash._compat import text_, text_type, binary_type, iteritems_, string_types, long, PY3 from backlash.utils import escape missing = object() @@ -45,7 +44,7 @@ def debug_repr(obj): - """Creates a debug repr of an object as HTML unicode string.""" + """Creates a debug repr of an object as HTML string.""" return DebugReprGenerator().repr(obj) @@ -61,7 +60,7 @@ def dump(obj=missing): sys.stdout._write(rv) -class _Helper(object): +class _Helper: """Displays an HTML version of the normal help, for the interactive debugger only because it requires a patched sys.stdout. """ @@ -75,7 +74,7 @@ def __call__(self, topic=None): return import pydoc pydoc.help(topic) - rv = text_(sys.stdout.reset(), 'utf-8', 'ignore') + rv = sys.stdout.reset() paragraphs = _paragraph_re.split(rv) if len(paragraphs) > 1: title = paragraphs[0] @@ -101,7 +100,7 @@ def _add_subclass_info(inner, obj, bases): return '%s%s(%s)' % (module, obj.__class__.__name__, inner) -class DebugReprGenerator(object): +class DebugReprGenerator: def __init__(self): self._stack = [] @@ -122,7 +121,7 @@ def proxy(self, obj, recursive): if have_extended_section: buf.append('') buf.append(right) - return _add_subclass_info(text_(''.join(buf)), obj, base) + return _add_subclass_info(''.join(buf), obj, base) return proxy list_repr = _sequence_repr_maker('[', ']', list) @@ -135,33 +134,7 @@ def proxy(self, obj, recursive): del _sequence_repr_maker def regex_repr(self, obj): - if PY3: - pattern = text_("'%s'" % str(obj.pattern), 'string-escape', 'ignore') - pattern = 'r' + pattern - else: - pattern = text_(repr(obj.pattern), 'string-escape', 'ignore') - if pattern[:1] == 'u': - pattern = 'ur' + pattern[1:] - else: - pattern = 'r' + pattern - return text_( - 're.compile(%s)' % pattern) - - def py2_string_repr(self, obj, limit=70): - buf = [''] - escaped = escape(obj) - a = repr(escaped[:limit]) - b = repr(escaped[limit:]) - if isinstance(obj, text_type): - buf.append('u') - a = a[1:] - b = b[1:] - if b != "''": - buf.extend((a[:-1], '', b[1:], '')) - else: - buf.append(a) - buf.append('') - return _add_subclass_info(text_('').join(buf), obj, (str, unicode)) + return 're.compile(r\'%s\')' % obj.pattern def py3_text_repr(self, obj, limit=70): buf = [''] @@ -173,11 +146,11 @@ def py3_text_repr(self, obj, limit=70): else: buf.append(a) buf.append('') - return _add_subclass_info(text_(''.join(buf)), obj, text_type) + return _add_subclass_info(''.join(buf), obj, str) def py3_binary_repr(self, obj, limit=70): buf = [''] - escaped = escape(text_(obj, 'utf-8', 'replace')) + escaped = escape(obj.decode('utf-8', 'replace')) a = repr(escaped[:limit]) b = repr(escaped[limit:]) buf.append('b') @@ -186,14 +159,14 @@ def py3_binary_repr(self, obj, limit=70): else: buf.append(a) buf.append('') - return _add_subclass_info(text_(''.join(buf)), obj, binary_type) + return _add_subclass_info(''.join(buf), obj, bytes) def dict_repr(self, d, recursive, limit=5): if recursive: - return _add_subclass_info(text_('{...}'), d, dict) + return _add_subclass_info('{...}', d, dict) buf = ['{'] have_extended_section = False - for idx, (key, value) in enumerate(iteritems_(d)): + for idx, (key, value) in enumerate(d.items()): if idx: buf.append(', ') if idx == limit - 1: @@ -205,25 +178,20 @@ def dict_repr(self, d, recursive, limit=5): if have_extended_section: buf.append('') buf.append('}') - return _add_subclass_info(text_(''.join(buf)), d, dict) + return _add_subclass_info(''.join(buf), d, dict) def object_repr(self, obj): - return text_('%s' % - escape(text_(repr(obj), 'utf-8', 'replace'))) + return '%s' % escape(repr(obj)) def dispatch_repr(self, obj, recursive): if obj is helper: - return text_('%r' % helper) - if isinstance(obj, (int, long, float, complex)): - return text_('%r' % obj) - if PY3: - if isinstance(obj, text_type): - return self.py3_text_repr(obj) - if isinstance(obj, binary_type): - return self.py3_binary_repr(obj) - else: - if isinstance(obj, basestring): - return self.py2_string_repr(obj) + return '%r' % helper + if isinstance(obj, (int, float, complex)): + return '%r' % obj + if isinstance(obj, str): + return self.py3_text_repr(obj) + if isinstance(obj, bytes): + return self.py3_binary_repr(obj) if isinstance(obj, RegexType): return self.regex_repr(obj) if isinstance(obj, list): @@ -245,10 +213,8 @@ def fallback_repr(self): info = ''.join(format_exception_only(*sys.exc_info()[:2])) except Exception: # pragma: no cover info = '?' - return text_( - '<broken repr (%s)>' - '' % escape(text_(info, 'utf-8', 'ignore').strip()) - ) + return ('<broken repr (%s)>' + '' % escape(info.strip())) def repr(self, obj): recursive = False @@ -270,8 +236,8 @@ def dump_object(self, obj): if isinstance(obj, dict): title = 'Contents of' items = [] - for key, value in iteritems_(obj): - if not isinstance(key, string_types): + for key, value in obj.items(): + if not isinstance(key, str): items = None break items.append((key, self.repr(value))) diff --git a/backlash/tbtools.py b/backlash/tbtools.py index 9aa3bc0..a318b9d 100644 --- a/backlash/tbtools.py +++ b/backlash/tbtools.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ werkzeug.debug.tbtools ~~~~~~~~~~~~~~~~~~~~~~ @@ -8,26 +7,22 @@ :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. :license: BSD. """ -import re +import inspect import os +import re import sys -import inspect import traceback -import codecs from tokenize import TokenError -from backlash.utils import escape from backlash.console import Console - -from backlash._compat import PY2, text_, native_, string_types, text_type, exec_ +from backlash.utils import escape _coding_re = re.compile(r'coding[:=]\s*([-\w.]+)') -_line_re = re.compile(r'^(.*?)$', re.MULTILINE) _funcdef_re = re.compile(r'^(\s*def\s)|(.*(? @@ -49,9 +44,9 @@