diff --git a/docs/conf.py b/docs/conf.py
index ee5058a..723fb5e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -28,7 +28,7 @@
# The short X.Y version
version = '3.0'
# The full version, including alpha/beta/rc tags
-release = '3.0.7'
+release = '3.0.8'
# -- General configuration ---------------------------------------------------
@@ -64,8 +64,7 @@
# (references to some Python built-in types do not resolve correctly)
nitpicky = True
nitpick_ignore = [('py:class', t) for t in (
- 'cgi.FieldStorage', 'html.parser.HTMLParser',
- 'threading.Thread', 'xmlrpc.client.ProtocolError')]
+ 'html.parser.HTMLParser', 'threading.Thread', 'xmlrpc.client.ProtocolError')]
# -- Options for HTML output -------------------------------------------------
diff --git a/docs/deploy.rst b/docs/deploy.rst
index 9aa5126..10724ed 100644
--- a/docs/deploy.rst
+++ b/docs/deploy.rst
@@ -486,7 +486,7 @@ In order to make use of Bjoern, you need to add the following at the end of the
Since Bjoern does not support the WSGI ``write()`` callable, you must configure Webware to not use this mechanism, by using the following settings at the top of the ``Scripts\WSGIScript.py``::
- settings = dict(WSGIWrite=False)
+ settings = {'WSGIWrite': False}
A systemd unit file at ``/etc/systemd/system/bjoern.service`` could look like this::
@@ -531,7 +531,7 @@ Add the following at the end of the ``Scripts\WSGIScript.py`` file in the applic
Similarly to Bjoern, you need to also adapt the settings at the top of the ``Scripts\WSGIScript.py`` file::
- settings = dict(WSGIWrite=False)
+ settings = {'WSGIWrite': False}
Using CherryPy as WSGI server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tox.ini b/tox.ini
index bcc8a9b..ad40ef7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ commands =
[testenv:pylint]
basepython = python3.10
-deps = pylint>=2.15,<3
+deps = pylint>=2.16,<3
commands =
pylint webware
diff --git a/webware/Admin/ServletCache.py b/webware/Admin/ServletCache.py
index d449094..b238145 100644
--- a/webware/Admin/ServletCache.py
+++ b/webware/Admin/ServletCache.py
@@ -70,7 +70,7 @@ def htCache(factory):
paths = []
for key in keys:
head, tail = os.path.split(key)
- path = dict(dir=head, base=tail, full=key, id=id(key))
+ path = {'dir': head, 'base': tail, 'full': key, 'id': id(key)}
paths.append(path)
paths.sort(key=lambda p: (p['base'].lower(), p['dir'].lower()))
for path in paths:
diff --git a/webware/Application.py b/webware/Application.py
index e6b4e6e..5f478b9 100644
--- a/webware/Application.py
+++ b/webware/Application.py
@@ -39,108 +39,104 @@
debug = False
-defaultConfig = dict(
- ActivityLogFilename='Activity.csv',
- ActivityLogColumns=[
+defaultConfig = {
+ 'ActivityLogFilename': 'Activity.csv',
+ 'ActivityLogColumns': [
'request.remoteAddress', 'request.method',
'request.uri', 'response.size',
'servlet.name', 'request.timeStamp',
- 'transaction.duration',
- 'transaction.errorOccurred'
+ 'transaction.duration', 'transaction.errorOccurred'
],
- AlwaysSaveSessions=True,
- AppLogFilename='Application.log',
- CacheDir='Cache',
- CacheServletClasses=True,
- CacheServletInstances=True,
- CheckInterval=None,
- Contexts={
+ 'AlwaysSaveSessions': True,
+ 'AppLogFilename': 'Application.log',
+ 'CacheDir': 'Cache',
+ 'CacheServletClasses': True,
+ 'CacheServletInstances': True,
+ 'CheckInterval': None,
+ 'Contexts': {
'default': 'Examples',
'Admin': 'Admin',
'Examples': 'Examples',
'Testing': 'Testing',
},
- Debug=dict(
- Sessions=False,
- ),
- DirectoryFile=['index', 'Index', 'main', 'Main'],
- EnterDebuggerOnException=False,
- EmailErrors=False,
- EmailErrorReportAsAttachment=False,
- ErrorEmailServer='localhost',
- ErrorEmailHeaders={
+ 'Debug': {'Sessions': False},
+ 'DirectoryFile': ['index', 'Index', 'main', 'Main'],
+ 'EnterDebuggerOnException': False,
+ 'EmailErrors': False,
+ 'EmailErrorReportAsAttachment': False,
+ 'ErrorEmailServer': 'localhost',
+ 'ErrorEmailHeaders': {
'From': 'webware@mydomain',
'To': ['webware@mydomain'],
'Reply-To': 'webware@mydomain',
'Content-Type': 'text/html',
'Subject': 'Error'
},
- ErrorLogFilename='Errors.csv',
- ErrorMessagesDir='ErrorMsgs',
- ErrorPage=None,
- ExtensionCascadeOrder=['.py', '.psp', '.html'],
- ExtensionsToIgnore={
+ 'ErrorLogFilename': 'Errors.csv',
+ 'ErrorMessagesDir': 'ErrorMsgs',
+ 'ErrorPage': None,
+ 'ExtensionCascadeOrder': ['.py', '.psp', '.html'],
+ 'ExtensionsToIgnore': {
'.pyc', '.pyo', '.tmpl', '.bak', '.py_bak',
'.py~', '.psp~', '.html~', '.tmpl~'
},
- ExtensionsToServe=[],
- ExtraPathInfo=True,
- FancyTracebackContext=5,
- FilesToHide={
+ 'ExtensionsToServe': [],
+ 'ExtraPathInfo': True,
+ 'FancyTracebackContext': 5, 'FilesToHide': {
'.*', '*~', '*.bak', '*.py_bak', '*.tmpl',
'*.pyc', '*.pyo', '__init__.*', '*.config'
},
- FilesToServe=[],
- IgnoreInvalidSession=True,
- IncludeEditLink=True,
- IncludeFancyTraceback=False,
- LogActivity=True,
- LogDir='Logs',
- LogErrors=True,
- MaxValueLengthInExceptionReport=500,
- OutputEncoding='utf-8',
- PlugIns=['MiscUtils', 'WebUtils', 'TaskKit', 'UserKit', 'PSP'],
- PrintConfigAtStartUp=True,
- PrintPlugIns=True,
- RegisterSignalHandler=False,
- ReloadServletClasses=False,
- ReportRPCExceptionsInWebware=True,
- ResponseBufferSize=8 * 1024, # 8 kBytes
- RetainSessions=True,
- RPCExceptionReturn='traceback',
- RunTasks=True,
- SaveErrorMessages=True,
- SecureSessionCookie=True,
- SessionCookiePath=None,
- HttpOnlySessionCookie=True,
- SameSiteSessionCookie='Strict',
- SessionModule='Session',
- SessionName='_SID_',
- SessionPrefix='',
- SessionStore='Dynamic',
- SessionStoreDir='Sessions',
- SessionTimeout=60,
- ShowDebugInfoOnErrors=False,
- SilentURIs=None,
- UnknownFileTypes=dict(
- ReuseServlets=True,
- Technique='serveContent', # or redirectSansScript
- CacheContent=False,
- MaxCacheContentSize=128 * 1024,
- ReadBufferSize=32 * 1024
- ),
- UseAutomaticPathSessions=False,
- UseCascadingExtensions=True,
- UseCookieSessions=True,
- UserErrorMessage=(
+ 'FilesToServe': [],
+ 'IgnoreInvalidSession': True,
+ 'IncludeEditLink': True,
+ 'IncludeFancyTraceback': False,
+ 'LogActivity': True,
+ 'LogDir': 'Logs',
+ 'LogErrors': True,
+ 'MaxValueLengthInExceptionReport': 500,
+ 'OutputEncoding': 'utf-8',
+ 'PlugIns': ['MiscUtils', 'WebUtils', 'TaskKit', 'UserKit', 'PSP'],
+ 'PrintConfigAtStartUp': True,
+ 'PrintPlugIns': True,
+ 'RegisterSignalHandler': False,
+ 'ReloadServletClasses': False,
+ 'ReportRPCExceptionsInWebware': True,
+ 'ResponseBufferSize': 8 * 1024, # 8 kBytes
+ 'RetainSessions': True,
+ 'RPCExceptionReturn': 'traceback',
+ 'RunTasks': True,
+ 'SaveErrorMessages': True,
+ 'SecureSessionCookie': True,
+ 'SessionCookiePath': None,
+ 'HttpOnlySessionCookie': True,
+ 'SameSiteSessionCookie': 'Strict',
+ 'SessionModule': 'Session',
+ 'SessionName': '_SID_',
+ 'SessionPrefix': '',
+ 'SessionStore': 'Dynamic',
+ 'SessionStoreDir': 'Sessions',
+ 'SessionTimeout': 60,
+ 'ShowDebugInfoOnErrors': False,
+ 'SilentURIs': None,
+ 'UnknownFileTypes': {
+ 'ReuseServlets': True,
+ 'Technique': 'serveContent', # or redirectSansScript
+ 'CacheContent': False,
+ 'MaxCacheContentSize': 128 * 1024,
+ 'ReadBufferSize': 32 * 1024
+ },
+ 'UseAutomaticPathSessions': False,
+ 'UseCascadingExtensions': True,
+ 'UseCookieSessions': True,
+ 'UserErrorMessage': (
'The site is having technical difficulties with this page.'
' An error has been logged, and the problem will be fixed'
' as soon as possible. Sorry!'
),
- UseSessionSweeper=True,
- Verbose=True,
- WSGIWrite=True, # use write callable with WSGI
-)
+ 'UseSessionSweeper': True,
+ 'Verbose': True,
+ 'WSGIWrite': True # use write callable with WSGI
+}
class EndResponse(Exception):
@@ -467,10 +463,11 @@ def configFilename(self):
def configReplacementValues(self):
"""Get config values that need to be escaped."""
- return dict(
- ServerSidePath=self._serverSidePath,
- WebwarePath=self._webwarePath,
- Development=self._development)
+ return {
+ 'ServerSidePath': self._serverSidePath,
+ 'WebwarePath': self._webwarePath,
+ 'Development': self._development
+ }
def development(self):
"""Whether the application shall run in development mode"""
@@ -645,12 +642,14 @@ def writeActivityLog(self, trans):
if mode == 'w':
f.write(','.join(self.setting('ActivityLogColumns')) + '\n')
values = []
- objects = dict(
- application=self, transaction=trans,
- request=trans.request(), response=trans.response(),
- servlet=trans.servlet(),
+ objects = {
+ 'application': self, 'transaction': trans,
+ 'request': trans.request(),
+ 'response': trans.response(),
+ 'servlet': trans.servlet(),
# don't cause creation of session here:
- session=trans._session)
+ 'session': trans._session
+ }
for column in self.setting('ActivityLogColumns'):
try:
value = valueForName(objects, column)
@@ -988,7 +987,7 @@ def handleExceptionInTransaction(self, excInfo, trans):
editlink = f'{request.scriptName()}/Admin/EditFile' if self.setting(
'IncludeEditLink') else None
self._exceptionHandlerClass(
- self, trans, excInfo, dict(editlink=editlink))
+ self, trans, excInfo, {'editlink': editlink})
def rootURLParser(self):
"""Accessor: the Root URL parser.
@@ -1172,7 +1171,7 @@ def loadPlugIns(self):
def __call__(self, environ, start_response):
"""The WSGI application callable"""
verbose = self._verbose
- requestDict = dict(environ=environ)
+ requestDict = {'environ': environ}
requestID = self._requestID
self._requestID = requestID + 1
diff --git a/webware/Examples/AjaxPage.py b/webware/Examples/AjaxPage.py
index d5242f1..ca8a902 100644
--- a/webware/Examples/AjaxPage.py
+++ b/webware/Examples/AjaxPage.py
@@ -41,12 +41,7 @@ def __str__(self):
def __call__(self, *args, **kw):
args = ','.join(map(quoteJs, args))
kwArgs = ','.join(f'{k}={quoteJs(v)}' for k, v in kw.items())
- if args and kwArgs:
- allArgs = f'{args},{kwArgs}'
- elif not kwArgs:
- allArgs = args
- elif not args:
- allArgs = kwArgs
+ allArgs = f'{args},{kwArgs}' if args and kwArgs else args or kwArgs
return self.__class__(f'{self}({allArgs}')
def __getitem__(self, index):
diff --git a/webware/Examples/DBUtilsDemo.py b/webware/Examples/DBUtilsDemo.py
index 05a00cf..2752d0f 100644
--- a/webware/Examples/DBUtilsDemo.py
+++ b/webware/Examples/DBUtilsDemo.py
@@ -200,8 +200,7 @@ def listSeminars(self):
pass
self.outputMsg(error, True)
return
- else:
- self.outputMsg(f'Entries deleted: {len(ids)}')
+ self.outputMsg(f'Entries deleted: {len(ids)}')
db = self.connection()
if not db:
return
@@ -271,8 +270,7 @@ def listAttendees(self):
db.rollback()
self.outputMsg(error, True)
return
- else:
- self.outputMsg(f'Entries deleted: {len(ids)}')
+ self.outputMsg(f'Entries deleted: {len(ids)}')
db = self.connection()
if not db:
return
diff --git a/webware/Examples/ExamplePage.py b/webware/Examples/ExamplePage.py
index b437a41..fce2a8f 100644
--- a/webware/Examples/ExamplePage.py
+++ b/webware/Examples/ExamplePage.py
@@ -57,8 +57,7 @@ def examplePages(self, plugInName=None):
plugIn = self.application().plugIn(plugInName)
except KeyError:
return []
- else:
- return plugIn.examplePages()
+ return plugIn.examplePages()
def writeSidebar(self):
self.writeExamplesMenu()
diff --git a/webware/Examples/FileUpload.py b/webware/Examples/FileUpload.py
index 963fc7f..6b91d7f 100644
--- a/webware/Examples/FileUpload.py
+++ b/webware/Examples/FileUpload.py
@@ -4,10 +4,10 @@
class FileUpload(ExamplePage):
"""This servlet shows how to handle uploaded files.
- The process is fairly self explanatory. You use a form like the one below
+ The process is fairly self-explanatory. You use a form like the one below
in the writeContent method. When the form is uploaded, the request field
with the name you gave to the file selector form item will be an instance
- of the FieldStorage class from the standard Python module "cgi". The key
+ of the FieldStorage class from the WebUtils.FieldStorage module. The key
attributes of this class are shown in the example below. The most important
things are filename, which gives the name of the file that was uploaded,
and file, which is an open file handle to the uploaded file. The uploaded
diff --git a/webware/Examples/ListBox.py b/webware/Examples/ListBox.py
index 8981604..5b7e806 100644
--- a/webware/Examples/ListBox.py
+++ b/webware/Examples/ListBox.py
@@ -20,8 +20,10 @@ def awake(self, transaction):
if session.hasValue('vars'):
self._vars = session.value('vars')
else:
- self._vars = dict(
- items=[], height=10, width=250, newCount=1, formCount=1)
+ self._vars = {
+ 'items': [], 'height': 10, 'width': 250,
+ 'newCount': 1, 'formCount': 1
+ }
session.setValue('vars', self._vars)
self._error = None
@@ -81,7 +83,7 @@ def widthChange():
def new(self):
"""Add a new item to the list box."""
newCount = self._vars['newCount']
- self._vars['items'].append(dict(name=f'New item {newCount}'))
+ self._vars['items'].append({'name': f'New item {newCount}'})
self._vars['newCount'] += 1
self.writeBody()
diff --git a/webware/Examples/SecurePage.py b/webware/Examples/SecurePage.py
index 43e1c23..4243d66 100644
--- a/webware/Examples/SecurePage.py
+++ b/webware/Examples/SecurePage.py
@@ -128,7 +128,7 @@ def loggedInUser(self):
return self.session().value('authenticated_user', None)
def defaultConfig(self):
- return dict(RequireLogin=True)
+ return {'RequireLogin': True}
def configFilename(self):
return 'Configs/SecurePage.config'
diff --git a/webware/HTTPRequest.py b/webware/HTTPRequest.py
index 7f0c8a9..f9d50cc 100644
--- a/webware/HTTPRequest.py
+++ b/webware/HTTPRequest.py
@@ -1,6 +1,5 @@
"""HTTP requests"""
-import cgi
import os
import sys
import traceback
@@ -11,7 +10,7 @@
import HTTPResponse
from MiscUtils import NoDefault
-from WebUtils import FieldStorage
+from WebUtils.FieldStorage import FieldStorage
from Request import Request
debug = False
@@ -36,19 +35,19 @@ def __init__(self, requestDict=None):
self._requestID = requestDict['requestID']
# Protect the loading of fields with an exception handler,
# because bad headers sometimes can break the field storage
- # (see also https://bugs.python.org/issue27777).
+ # (see also https://github.com/python/cpython/issues/71964).
try:
- self._fields = FieldStorage.FieldStorage(
+ self._fields = FieldStorage(
self._input, environ=self._environ,
keep_blank_values=True, strict_parsing=False)
except Exception:
- self._fields = cgi.FieldStorage(keep_blank_values=True)
+ self._fields = FieldStorage(keep_blank_values=True)
traceback.print_exc(file=sys.stderr)
self._cookies = Cookie()
if 'HTTP_COOKIE' in self._environ:
# If there are duplicate cookies, always use the first one
# because it is the most relevant one according to RFC 2965
- # (workaround for https://bugs.python.org/issue1375011).
+ # (see also https://github.com/python/cpython/issues/42664).
# noinspection PyTypeChecker
cookies = dict(cookie.split('=', 1) for cookie in reversed(
self._environ['HTTP_COOKIE'].split('; ')))
@@ -64,7 +63,7 @@ def __init__(self, requestDict=None):
self._time = time()
self._environ = os.environ.copy()
self._input = None
- self._fields = cgi.FieldStorage(keep_blank_values=True)
+ self._fields = FieldStorage(keep_blank_values=True)
self._cookies = Cookie()
env = self._environ
@@ -131,8 +130,8 @@ def __init__(self, requestDict=None):
# We use Tim O'Malley's Cookie class to get the cookies,
# but then change them into an ordinary dictionary of values
- self._cookies = dict(
- (key, self._cookies[key].value) for key in self._cookies)
+ self._cookies = {
+ key: self._cookies[key].value for key in self._cookies}
self._contextName = None
self._serverSidePath = self._serverSideContextPath = None
@@ -322,7 +321,7 @@ def urlPath(self):
This is actually the same as pathInfo().
- For example, http://host/Webware/Context/Servlet?x=1
+ For example, https://host/Webware/Context/Servlet?x=1
yields '/Context/Servlet'.
"""
return self._pathInfo
@@ -330,7 +329,7 @@ def urlPath(self):
def urlPathDir(self):
"""Same as urlPath, but only gives the directory.
- For example, http://host/Webware/Context/Servlet?x=1
+ For example, https://host/Webware/Context/Servlet?x=1
yields '/Context'.
"""
return os.path.dirname(self.urlPath())
@@ -435,7 +434,7 @@ def serverURL(self, canonical=False):
then the canonical hostname of the server is used if possible.
The path is returned without any extra path info or query strings,
- i.e. http://www.my.own.host.com:8080/Webware/TestPage.py
+ i.e. https://www.my.own.host.com:8080/Webware/TestPage.py
"""
if canonical and 'SCRIPT_URI' in self._environ:
return self._environ['SCRIPT_URI']
@@ -802,7 +801,8 @@ def info(self):
]
for method in _infoMethods:
try:
- info.append((method.__name__, method(self)))
+ # noinspection PyArgumentList
+ info.append((method.__name__, method(self),))
except Exception:
info.append((method.__name__, None))
return info
diff --git a/webware/HTTPResponse.py b/webware/HTTPResponse.py
index c55e6ca..bed574a 100644
--- a/webware/HTTPResponse.py
+++ b/webware/HTTPResponse.py
@@ -441,7 +441,7 @@ def rawResponse(self):
headers.append((key, value))
for cookie in self._cookies.values():
headers.append(('Set-Cookie', cookie.headerValue()))
- return dict(headers=headers, contents=self._strmOut.buffer())
+ return {'headers': headers, 'contents': self._strmOut.buffer()}
def size(self):
"""Return the size of the final contents of the response.
diff --git a/webware/MiscUtils/DataTable.py b/webware/MiscUtils/DataTable.py
index 9a5aac8..1e79826 100644
--- a/webware/MiscUtils/DataTable.py
+++ b/webware/MiscUtils/DataTable.py
@@ -13,7 +13,7 @@
John 893-5901
This data often comes from delimited text files which typically
-have well defined columns or fields with several rows each of which can
+have well-defined columns or fields with several rows each of which can
be thought of as a record.
Using a DataTable can be as easy as using lists and dictionaries::
@@ -202,6 +202,7 @@
from datetime import date, datetime, time, timedelta, tzinfo
from decimal import Decimal
+from time import sleep
from warnings import warn
from MiscUtils import NoDefault
@@ -249,12 +250,11 @@
def canReadExcel():
try:
- from win32com.client import Dispatch # pylint: disable=import-error
- Dispatch("Excel.Application")
+ from win32com import client # pylint: disable=import-error
+ client.gencache.EnsureDispatch("Excel.Application")
except Exception:
return False
- else:
- return True
+ return True
# endregion Functions
@@ -441,9 +441,19 @@ def canReadExcel():
def readExcel(self, worksheet=1, row=1, column=1):
maxBlankRows = 10
numRowsToReadPerCall = 20
- from win32com.client import Dispatch # pylint: disable=import-error
- excel = Dispatch("Excel.Application")
- workbook = excel.Workbooks.Open(os.path.abspath(self._filename))
+ from win32com import client # pylint: disable=import-error
+ for _retry in range(40):
+ try:
+ excel = client.gencache.EnsureDispatch("Excel.Application")
+ workbooks = excel.Workbooks
+ except Exception:
+ # Excel may be busy, wait a bit and try again
+ sleep(0.125)
+ else:
+ break
+ else:
+ raise DataTableError('Cannot read Excel sheet')
+ workbook = workbooks.Open(os.path.abspath(self._filename))
try:
worksheet = workbook.Worksheets(worksheet)
worksheet.Cells(row, column)
diff --git a/webware/MiscUtils/DictForArgs.py b/webware/MiscUtils/DictForArgs.py
index 99dfd37..c660e73 100644
--- a/webware/MiscUtils/DictForArgs.py
+++ b/webware/MiscUtils/DictForArgs.py
@@ -7,6 +7,16 @@
import re
+verbose = False
+
+_nameRE = re.compile(r'\w+')
+_equalsRE = re.compile(r'\=')
+_stringRE = re.compile(r'''"[^"]+"|'[^']+'|\S+''')
+_whiteRE = re.compile(r'\s+')
+
+_REs = {_nameRE: 'name', _equalsRE: 'equals',
+ _stringRE: 'string', _whiteRE: 'white'}
+
class DictForArgsError(Exception):
"""Error when building dictionary from arguments."""
@@ -16,14 +26,6 @@ def _SyntaxError(s):
raise DictForArgsError(f'Syntax error: {s!r}')
-_nameRE = re.compile(r'\w+')
-_equalsRE = re.compile(r'\=')
-_stringRE = re.compile(r'''"[^"]+"|'[^']+'|\S+''')
-_whiteRE = re.compile(r'\s+')
-
-_REs = [_nameRE, _equalsRE, _stringRE, _whiteRE]
-
-
def dictForArgs(s):
"""Build dictionary from arguments.
@@ -58,7 +60,6 @@ def dictForArgs(s):
# Tokenize
- verbose = False
matches = []
start = 0
sLen = len(s)
@@ -84,17 +85,7 @@ def dictForArgs(s):
_SyntaxError(s)
if verbose:
- names = []
- for match in matches:
- if match.re is _nameRE:
- name = 'name'
- elif match.re is _equalsRE:
- name = 'equals'
- elif match.re is _stringRE:
- name = 'string'
- elif match.re is _whiteRE:
- name = 'white'
- names.append(name)
+ names = ', '.join(_REs[match.re] for match in matches)
print('>> names =', names)
# Process tokens
@@ -107,10 +98,7 @@ def dictForArgs(s):
i = 0
while i < matchesLen:
match = matches[i]
- if i + 1 < matchesLen:
- peekMatch = matches[i+1]
- else:
- peekMatch = None
+ peekMatch = matches[i+1] if i + 1 < matchesLen else None
if match.re is _nameRE:
if peekMatch is not None:
if peekMatch.re is _nameRE:
@@ -121,9 +109,9 @@ def dictForArgs(s):
if peekMatch.re is _equalsRE:
if i + 2 < matchesLen:
target = matches[i+2]
- if target.re is _nameRE or target.re is _stringRE:
+ if target.re in (_nameRE, _stringRE):
value = target.group()
- if value[0] == "'" or value[0] == '"':
+ if value[0] in ("'", '"'):
value = value[1:-1]
d[match.group()] = value
i += 3
diff --git a/webware/MiscUtils/Funcs.py b/webware/MiscUtils/Funcs.py
index 98039c8..55b26d0 100644
--- a/webware/MiscUtils/Funcs.py
+++ b/webware/MiscUtils/Funcs.py
@@ -268,11 +268,12 @@ def timestamp(t=None):
formats are generally more appropriate for filenames.
"""
t = time.localtime(t)[:6]
- return dict(
- tuple=t,
- pretty='{:4d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(*t),
- condensed='{:4d}{:02d}{:02d}{:02d}{:02d}{:02d}'.format(*t),
- dashed='{:4d}-{:02d}-{:02d}-{:02d}-{:02d}-{:02d}'.format(*t))
+ return {
+ 'tuple': t,
+ 'pretty': '{:4d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(*t),
+ 'condensed': '{:4d}{:02d}{:02d}{:02d}{:02d}{:02d}'.format(*t),
+ 'dashed': '{:4d}-{:02d}-{:02d}-{:02d}-{:02d}-{:02d}'.format(*t)
+ }
def localTimeDelta(t=None):
diff --git a/webware/MiscUtils/Tests/TestCSVParser.py b/webware/MiscUtils/Tests/TestCSVParser.py
index 6f89ae2..da7c5ee 100644
--- a/webware/MiscUtils/Tests/TestCSVParser.py
+++ b/webware/MiscUtils/Tests/TestCSVParser.py
@@ -21,14 +21,10 @@ def testNegatives(self):
'a\n,b'
]
for inp in inputs:
- try:
+ with self.assertRaises(
+ ParseError, msg=f'Did not get an exception for: {inp!r}'):
results = self.parse(inp)
- except ParseError:
- pass
- else:
- print()
- print('results:', repr(results))
- raise Exception(f'Did not get an exception for: {inp!r}')
+ print(f'\nresults: {results!r}')
def testPositives(self):
tests = [
@@ -86,19 +82,16 @@ def testPositives(self):
self.assertEqual(
res, out,
f'\ninput={inp!r}\nresult={res!r}\noutput={out!r}')
- res = self.parse(inp+'\n')
+ res = self.parse(inp + '\n')
self.assertEqual(
res, out,
f'\ninput={inp!r}\nresult={res!r}\noutput={out!r}')
else:
# multiple lines
- gotFields = False
+ res = None
for line in inp.splitlines():
- self.assertFalse(gotFields)
+ self.assertIsNone(res)
res = self.parse(line)
- if res is not None:
- gotFields = True
- self.assertTrue(gotFields)
self.assertEqual(
res, out,
f'\ninput={inp!r}\nresult={res!r}\noutput={out!r}')
diff --git a/webware/MiscUtils/Tests/TestDataTable.py b/webware/MiscUtils/Tests/TestDataTable.py
index 0c66287..ad728cc 100644
--- a/webware/MiscUtils/Tests/TestDataTable.py
+++ b/webware/MiscUtils/Tests/TestDataTable.py
@@ -98,7 +98,7 @@ def _testBasic(self):
data = [
['John', '26', '7.25'],
['Mary', 32, 8.5],
- dict(name='Fred', age=28, rating=9.0),
+ {'name': 'Fred', 'age': 28, 'rating': 9.0},
Record(name='Wilma', age=27, rating=9.5)
]
for obj in data:
@@ -212,13 +212,10 @@ def testBasics(self):
self._testSource('MK enums', src, headings, data)
# Unfinished multiline record
- try:
+ with self.assertRaises(
+ DataTableError,
+ msg='Did not raise error for unfinished multiline record'):
DataTable().readString('a\n"1\n')
- except DataTableError:
- pass # just what we were expecting
- else:
- raise Exception(
- 'Failed to raise exception for unfinished multiline record')
def testDefaultUsePickleCache(self):
t = DataTable()
diff --git a/webware/MiscUtils/Tests/TestFuncs.py b/webware/MiscUtils/Tests/TestFuncs.py
index bf851ad..d49a21c 100644
--- a/webware/MiscUtils/Tests/TestFuncs.py
+++ b/webware/MiscUtils/Tests/TestFuncs.py
@@ -85,20 +85,20 @@ def testHostName(self):
self.assertEqual(host, host.lower())
def testLocalIP(self):
- ip = localIP()
- self.assertTrue(ip)
- self.assertFalse(ip.startswith('127.'))
- self.assertEqual(localIP(), ip) # second invocation
- self.assertEqual(localIP(useCache=None), ip)
- # ignore if the following tests fetch the WSL address
- ips = (ip, '192.168.80.1', '172.22.32.1', '172.25.112.1')
- self.assertIn(
- localIP(remote=None, useCache=None), ips,
- 'See if this works: localIP(remote=None).'
- ' If this fails, dont worry.')
- self.assertIn(
- localIP(remote=('www.hostname.and.domain.are.invalid', 80),
- useCache=None), ips)
+ thisIp = localIP()
+ self.assertTrue(thisIp)
+ self.assertFalse(thisIp.startswith('127.'))
+ self.assertEqual(localIP(), thisIp) # second invocation
+ self.assertEqual(localIP(useCache=None), thisIp)
+
+ def assertLocal(ip):
+ self.assertTrue(
+ ip == thisIp or ip.startswith(('127.', '192.168.'))
+ or '172.16.' <= ip <= '172.31.')
+
+ assertLocal(localIP(remote=None, useCache=None))
+ assertLocal(localIP(remote=('www.hostname.and.domain.are.invalid', 80),
+ useCache=None))
def testPositiveId(self):
# About all we can do is invoke positiveId()
diff --git a/webware/MiscUtils/Tests/TestPickleCache.py b/webware/MiscUtils/Tests/TestPickleCache.py
index 52aef81..d040308 100644
--- a/webware/MiscUtils/Tests/TestPickleCache.py
+++ b/webware/MiscUtils/Tests/TestPickleCache.py
@@ -31,7 +31,7 @@ def testOneIteration(self):
sourcePath = self._sourcePath = os.path.join(self._tempDir, 'foo.dict')
picklePath = self._picklePath = pc.PickleCache().picklePath(sourcePath)
self.remove(picklePath) # make sure we're clean
- data = self._data = dict(x=1)
+ data = self._data = {'x': 1}
self.writeSource()
try:
# test 1: no pickle cache yet
diff --git a/webware/MockApplication.py b/webware/MockApplication.py
index 767ce70..aaa974a 100644
--- a/webware/MockApplication.py
+++ b/webware/MockApplication.py
@@ -9,11 +9,11 @@ def recordFile(self, filename, isfile=None):
pass
-defaultConfig = dict(
- CacheDir='Cache',
- PlugIns=['MiscUtils', 'WebUtils', 'TaskKit', 'UserKit', 'PSP'],
- PrintPlugIns=False
-)
+defaultConfig = {
+ 'CacheDir': 'Cache',
+ 'PlugIns': ['MiscUtils', 'WebUtils', 'TaskKit', 'UserKit', 'PSP'],
+ 'PrintPlugIns': False
+}
class MockApplication(ConfigurableForServerSidePath):
@@ -51,10 +51,11 @@ def defaultConfig(self):
def configReplacementValues(self):
"""Get config values that need to be escaped."""
- return dict(
- ServerSidePath=self._serverSidePath,
- WebwarePath=self._webwarePath,
- Development=self._development)
+ return {
+ 'ServerSidePath': self._serverSidePath,
+ 'WebwarePath': self._webwarePath,
+ 'Development': self._development
+ }
def configFilename(self):
return self.serverSidePath('Configs/Application.config')
diff --git a/webware/PSP/BraceConverter.py b/webware/PSP/BraceConverter.py
index 5712165..da2f645 100644
--- a/webware/PSP/BraceConverter.py
+++ b/webware/PSP/BraceConverter.py
@@ -77,7 +77,7 @@ def parseLine(self, line, writer):
self.line = ''
else:
# should never get here
- raise Exception()
+ raise ValueError(f'Invalid character: {c!r}')
writer.printChars('\n')
def openBlock(self, writer):
diff --git a/webware/PSP/ParseEventHandler.py b/webware/PSP/ParseEventHandler.py
index 71aa489..d47e31a 100644
--- a/webware/PSP/ParseEventHandler.py
+++ b/webware/PSP/ParseEventHandler.py
@@ -36,16 +36,12 @@ class ParseEventHandler:
"""
aspace = ' '
- defaults = dict(
- BASE_CLASS='Page',
- BASE_METHOD='writeHTML',
- imports=dict(filename='classes'),
- threadSafe='no',
- instanceSafe='yes',
- indent=4,
- gobbleWhitespace=True,
- formatter='str'
- )
+ defaults = {
+ 'BASE_CLASS': 'Page', 'BASE_METHOD': 'writeHTML',
+ 'imports': {'filename': 'classes'}, 'threadSafe': 'no',
+ 'instanceSafe': 'yes', 'indent': 4,
+ 'gobbleWhitespace': True, 'formatter': 'str'
+ }
def __init__(self, ctxt, parser):
self._ctxt = ctxt
diff --git a/webware/PickleRPCServlet.py b/webware/PickleRPCServlet.py
index 208f351..1d36ddd 100644
--- a/webware/PickleRPCServlet.py
+++ b/webware/PickleRPCServlet.py
@@ -80,7 +80,7 @@ def respondToPost(self, trans):
try:
request = trans.request()
data = request.rawInput(rewind=1)
- response = dict(timeReceived=trans.request().time())
+ response = {'timeReceived': trans.request().time()}
try:
try:
encoding = request.environ().get('HTTP_CONTENT_ENCODING')
diff --git a/webware/Properties.py b/webware/Properties.py
index 491aea0..ca1f447 100644
--- a/webware/Properties.py
+++ b/webware/Properties.py
@@ -1,6 +1,6 @@
name = 'Webware for Python'
-version = (3, 0, 7)
+version = (3, 0, 8)
status = 'stable'
diff --git a/webware/Servlet.py b/webware/Servlet.py
index 7189ab9..239ba67 100644
--- a/webware/Servlet.py
+++ b/webware/Servlet.py
@@ -81,11 +81,9 @@ def runTransaction(transaction):
# just a result of the first. Then you're stuck scratching your
# head wondering what the first might have been.
raise second from first
- else:
- # no problems with sleep() so raise the one and only exception
- raise
- else:
- transaction.sleep()
+ # no problems with sleep() so raise the one and only exception
+ raise
+ transaction.sleep()
def runMethodForTransaction(self, transaction, method, *args, **kw):
self.awake(transaction)
diff --git a/webware/SessionMemcachedStore.py b/webware/SessionMemcachedStore.py
index 39109f2..6dc5d48 100644
--- a/webware/SessionMemcachedStore.py
+++ b/webware/SessionMemcachedStore.py
@@ -212,9 +212,8 @@ def pop(self, key, default=NoDefault):
value = self[key]
except KeyError:
return default
- else:
- del self[key]
- return value
+ del self[key]
+ return value
# endregion Access
diff --git a/webware/SessionRedisStore.py b/webware/SessionRedisStore.py
index 81b817f..9c80fc3 100644
--- a/webware/SessionRedisStore.py
+++ b/webware/SessionRedisStore.py
@@ -188,9 +188,8 @@ def pop(self, key, default=NoDefault):
value = self[key]
except KeyError:
return default
- else:
- del self[key]
- return value
+ del self[key]
+ return value
# endregion Access
diff --git a/webware/Testing/Main.py b/webware/Testing/Main.py
index c9d2e12..f1f9cc2 100644
--- a/webware/Testing/Main.py
+++ b/webware/Testing/Main.py
@@ -76,8 +76,8 @@ def readContent(self, content):
if len(parts) != 2:
self.error(f'Invalid line at {lineNum}.')
urls.append(parts[0].strip())
- cases.append(dict(
- URLs=urls, Expectation=parts[1].strip()))
+ cases.append(
+ {'URLs': urls, 'Expectation': parts[1].strip()})
urls = [] # reset list of URLs
lineNum += 1
return cases
@@ -92,4 +92,4 @@ def writeNotes(self):
''')
def error(self, msg):
- raise Exception(msg)
+ raise RuntimeError(msg)
diff --git a/webware/Tests/TestEndToEnd/AppTest.py b/webware/Tests/TestEndToEnd/AppTest.py
index 0751d0c..52d44d5 100644
--- a/webware/Tests/TestEndToEnd/AppTest.py
+++ b/webware/Tests/TestEndToEnd/AppTest.py
@@ -14,9 +14,7 @@
class AppTest:
- settings = dict(
- PrintConfigAtStartUp=False
- )
+ settings = {'PrintConfigAtStartUp': False}
catchOutput = True # set to False if you want to run pdb inside a test
@classmethod
diff --git a/webware/Tests/TestEndToEnd/TestAdmin.py b/webware/Tests/TestEndToEnd/TestAdmin.py
index ca9801f..d408a14 100644
--- a/webware/Tests/TestEndToEnd/TestAdmin.py
+++ b/webware/Tests/TestEndToEnd/TestAdmin.py
@@ -7,10 +7,10 @@
class TestAdminLogin(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False,
- AdminPassword='LoginTestPassword'
- )
+ settings = {
+ 'PrintConfigAtStartUp': False,
+ 'AdminPassword': 'LoginTestPassword'
+ }
def testLogin(self):
r = self.testApp.get('/')
@@ -79,12 +79,11 @@ def testLogin(self):
class TestAdmin(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False,
- AdminPassword='AdminTestPassword',
- CacheServletClasses=True,
- CacheServletInstances=True
- )
+ settings = {
+ 'PrintConfigAtStartUp': False,
+ 'AdminPassword': 'AdminTestPassword',
+ 'CacheServletClasses': True, 'CacheServletInstances': True
+ }
def setUp(self):
AppTest.setUp(self)
diff --git a/webware/Tests/TestEndToEnd/TestContextMap.py b/webware/Tests/TestEndToEnd/TestContextMap.py
index 2b71944..4d935c0 100644
--- a/webware/Tests/TestEndToEnd/TestContextMap.py
+++ b/webware/Tests/TestEndToEnd/TestContextMap.py
@@ -7,11 +7,10 @@
class TestContextMap(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False,
- ExtraPathInfo=False,
- Contexts={'Testing': 'Testing', 'default': 'Testing'}
- )
+ settings = {
+ 'PrintConfigAtStartUp': False, 'ExtraPathInfo': False,
+ 'Contexts': {'Testing': 'Testing', 'default': 'Testing'}
+ }
def testStartPage(self):
r = self.testApp.get('/')
@@ -34,11 +33,9 @@ def testBadContextPage(self):
class TestAliasedContext(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False,
- ExtraPathInfo=False,
- Contexts={'TestAlias': 'Testing', 'default': 'TestAlias'}
- )
+ settings = {
+ 'PrintConfigAtStartUp': False, 'ExtraPathInfo': False,
+ 'Contexts': {'TestAlias': 'Testing', 'default': 'TestAlias'}}
def testStartPage(self):
r = self.testApp.get('/')
@@ -61,11 +58,10 @@ def testBadContextPage(self):
class TestOnlyDefaultContext(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False,
- ExtraPathInfo=False,
- Contexts={'default': 'Testing'}
- )
+ settings = {
+ 'PrintConfigAtStartUp': False, 'ExtraPathInfo': False,
+ 'Contexts': {'default': 'Testing'}
+ }
def testStartPage(self):
r = self.testApp.get('/')
diff --git a/webware/Tests/TestEndToEnd/TestExamples.py b/webware/Tests/TestEndToEnd/TestExamples.py
index 00bfa84..fae6fe3 100644
--- a/webware/Tests/TestEndToEnd/TestExamples.py
+++ b/webware/Tests/TestEndToEnd/TestExamples.py
@@ -9,9 +9,7 @@
class TestExamples(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False
- )
+ settings = {'PrintConfigAtStartUp': False}
def setUp(self):
AppTest.setUp(self)
@@ -402,8 +400,7 @@ def testRequestInformation(self):
'>cookies()<',
'
TestCookieName | CookieValue | ',
no=['Reload the page', 'add some test fields'])
- r = r.goto('/RequestInformation',
- extra_environ=dict(REMOTE_USER='bob'))
+ r = r.goto('/RequestInformation', extra_environ={'REMOTE_USER': 'bob'})
self.assertEqual(r.status, '200 OK')
r.mustcontain(
'Request Variables
',
@@ -626,17 +623,13 @@ def testJSONRPCClient(self):
class TestExamplesWithoutWSGIWrite(TestExamples):
- settings = dict(
- PrintConfigAtStartUp=False,
- WSGIWrite=False
- )
+ settings = {'PrintConfigAtStartUp': False, 'WSGIWrite': False}
class TestExamplesWithoutCaching(TestExamples):
- settings = dict(
- PrintConfigAtStartUp=False,
- CacheServletClasses=False,
- CacheServletInstances=False,
- ReloadServletClasses=False
- )
+ settings = {
+ 'PrintConfigAtStartUp': False,
+ 'CacheServletClasses': False, 'CacheServletInstances': False,
+ 'ReloadServletClasses': False
+ }
diff --git a/webware/Tests/TestEndToEnd/TestPSPExamples.py b/webware/Tests/TestEndToEnd/TestPSPExamples.py
index ad65884..e0f008c 100644
--- a/webware/Tests/TestEndToEnd/TestPSPExamples.py
+++ b/webware/Tests/TestEndToEnd/TestPSPExamples.py
@@ -7,10 +7,7 @@
class TestPSPExamples(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False,
- ClearPSPCacheOnStart=True
- )
+ settings = {'PrintConfigAtStartUp': False, 'ClearPSPCacheOnStart': True}
def testStartPage(self):
r = self.testApp.get('/').click('^PSP$')
diff --git a/webware/Tests/TestEndToEnd/TestTesting.py b/webware/Tests/TestEndToEnd/TestTesting.py
index a6b8932..f4bc91f 100644
--- a/webware/Tests/TestEndToEnd/TestTesting.py
+++ b/webware/Tests/TestEndToEnd/TestTesting.py
@@ -8,10 +8,7 @@
class TestTesting(AppTest, unittest.TestCase):
- settings = dict(
- PrintConfigAtStartUp=False,
- ExtraPathInfo=False
- )
+ settings = {'PrintConfigAtStartUp': False, 'ExtraPathInfo': False}
def testStartPage(self):
r = self.testApp.get('/').click('Testing')
@@ -273,7 +270,4 @@ def testCase18(self):
class TestTestingWithExtraPathInfo(TestTesting):
- settings = dict(
- PrintConfigAtStartUp=False,
- ExtraPathInfo=True
- )
+ settings = {'PrintConfigAtStartUp': False, 'ExtraPathInfo': True}
diff --git a/webware/Tests/TestSessions/Application.py b/webware/Tests/TestSessions/Application.py
index d265ebb..0eb391a 100644
--- a/webware/Tests/TestSessions/Application.py
+++ b/webware/Tests/TestSessions/Application.py
@@ -12,18 +12,17 @@ class Application:
_sessionName = '_SID_'
def setting(self, key, default=None):
- return dict(
- SessionTimeout=self._sessionTimeout // 60,
- SessionPrefix=self._sessionPrefix,
- SessionName=self._sessionName,
- DynamicSessionTimeout=1,
- MaxDynamicMemorySessions=3,
- MemcachedOnIteration=None,
- Debug=dict(Sessions=False),
- ).get(key, default)
+ return {
+ 'SessionTimeout': self._sessionTimeout // 60,
+ 'SessionPrefix': self._sessionPrefix,
+ 'SessionName': self._sessionName,
+ 'DynamicSessionTimeout': 1, 'MaxDynamicMemorySessions': 3,
+ 'MemcachedOnIteration': None,
+ 'Debug': {'Sessions': False}
+ }.get(key, default)
def handleException(self):
- raise Exception('Application Error')
+ raise RuntimeError('Application Error')
def sessionTimeout(self, _trans=None):
return self._sessionTimeout
diff --git a/webware/Tests/TestSessions/Session.py b/webware/Tests/TestSessions/Session.py
index 72bacfb..3938076 100644
--- a/webware/Tests/TestSessions/Session.py
+++ b/webware/Tests/TestSessions/Session.py
@@ -10,7 +10,7 @@ class Session:
def __init__(self, identifier=7, value=None):
self._identifier = f'foo-{identifier}'
- self._data = dict(bar=value or identifier * 6)
+ self._data = {'bar': value or identifier * 6}
self._expired = self._dirty = False
self._timeout = 1800
self._lastAccessTime = time() - identifier * 400
diff --git a/webware/UnknownFileTypeServlet.py b/webware/UnknownFileTypeServlet.py
index 1e59dbf..09acdf1 100644
--- a/webware/UnknownFileTypeServlet.py
+++ b/webware/UnknownFileTypeServlet.py
@@ -226,10 +226,11 @@ def serveContent(self, trans):
and fileSize < maxCacheContentSize):
if debug:
print('>> caching')
- fileDict = dict(
- content=f.read(),
- mimeType=mimeType, mimeEncoding=mimeEncoding,
- mtime=mtime, size=fileSize, filename=filename)
+ fileDict = {
+ 'content': f.read(),
+ 'mimeType': mimeType, 'mimeEncoding': mimeEncoding,
+ 'mtime': mtime, 'size': fileSize, 'filename': filename
+ }
fileCache[filename] = fileDict
if fileDict is not None:
if debug:
diff --git a/webware/WebUtils/FieldStorage.py b/webware/WebUtils/FieldStorage.py
index af9f6aa..9f6eb06 100644
--- a/webware/WebUtils/FieldStorage.py
+++ b/webware/WebUtils/FieldStorage.py
@@ -187,6 +187,8 @@ def __init__(self, fp=None, headers=None, outerboundary=b'',
ctype, pdict = 'application/x-www-form-urlencoded', {}
self.type = ctype
self.type_options = pdict
+ if not self._binary_file and isBinaryType(ctype, pdict):
+ self._binary_file = True
self.innerboundary = pdict['boundary'].encode(
self.encoding, self.errors) if 'boundary' in pdict else b''
@@ -416,7 +418,6 @@ def read_single(self):
def read_binary(self):
"""Internal: read binary data."""
- self.file = self.make_file()
todo = self.length
while todo > 0:
data = self.fp.read(min(todo, self.bufsize))
@@ -428,10 +429,14 @@ def read_binary(self):
if not data:
self.done = -1
break
- if self._binary_file:
- self.file.write(data)
- else: # fix for issue 27777
- self.file.write(data.decode())
+ if not self._binary_file:
+ # fix for https://github.com/python/cpython/issues/71964
+ try:
+ data = data.decode()
+ except UnicodeDecodeError:
+ self._binary_file = True
+ self.file = self.make_file()
+ self.file.write(data)
todo -= len(data)
def read_lines(self):
@@ -634,3 +639,13 @@ def hasSeparator():
except TypeError: # Python < 3.9.2
return False
return True
+
+
+def isBinaryType(ctype, pdict=None):
+ """"Check whether the given MIME type uses binary data."""
+ if pdict and pdict.get('charset') == 'binary':
+ return True
+ return not (
+ ctype.startswith('text/') or ctype.endswith(('+json', '+xml')) or
+ (ctype.startswith('application') and
+ ctype.endswith(('/json', '/xml', '/ecmascript', '/javascript'))))
diff --git a/webware/WebUtils/HTMLTag.py b/webware/WebUtils/HTMLTag.py
index f0e7d60..3a35dd1 100644
--- a/webware/WebUtils/HTMLTag.py
+++ b/webware/WebUtils/HTMLTag.py
@@ -683,8 +683,8 @@ def encounteredTag(self, tag, lineNum):
cannotHave=self.tags)
-configClassForName = dict(
- canOnlyHave=TagCanOnlyHaveConfig, cannotHave=TagCannotHaveConfig)
+configClassForName = {
+ 'canOnlyHave': TagCanOnlyHaveConfig, 'cannotHave': TagCannotHaveConfig}
if __name__ == '__main__':
diff --git a/webware/WebUtils/HTTPStatusCodes.py b/webware/WebUtils/HTTPStatusCodes.py
index 0d35659..21be35a 100644
--- a/webware/WebUtils/HTTPStatusCodes.py
+++ b/webware/WebUtils/HTTPStatusCodes.py
@@ -21,9 +21,9 @@
# Construct HTTPStatusCodes dictionary
for code, identifier, description in HTTPStatusCodeList:
- d = dict(code=code, identifier=identifier, description=description,
- # the following tow exist for backward compatibility only:
- htmlMsg=description, asciiMsg=description)
+ d = {'code': code, 'identifier': identifier, 'description': description,
+ # the following two exist for backward compatibility only:
+ 'htmlMsg': description, 'asciiMsg': description}
HTTPStatusCodes[code] = d
HTTPStatusCodes[identifier] = d
diff --git a/webware/WebUtils/Tests/TestFieldStorageModified.py b/webware/WebUtils/Tests/TestFieldStorageModified.py
index 510f956..d55551f 100644
--- a/webware/WebUtils/Tests/TestFieldStorageModified.py
+++ b/webware/WebUtils/Tests/TestFieldStorageModified.py
@@ -4,14 +4,14 @@
from io import BytesIO
-from WebUtils.FieldStorage import FieldStorage, hasSeparator
+from WebUtils.FieldStorage import FieldStorage, hasSeparator, isBinaryType
class TestFieldStorage(unittest.TestCase):
def testGetRequest(self):
- fs = FieldStorage(environ=dict(
- REQUEST_METHOD='GET', QUERY_STRING='a=1&b=2&b=3&c=3'))
+ fs = FieldStorage(environ={
+ 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'a=1&b=2&b=3&c=3'})
self.assertEqual(fs.getfirst('a'), '1')
self.assertEqual(fs.getfirst('b'), '2')
self.assertEqual(fs.getfirst('c'), '3')
@@ -20,8 +20,8 @@ def testGetRequest(self):
self.assertEqual(fs.getlist('c'), ['3'])
def testPostRequestWithQuery(self):
- fs = FieldStorage(fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1&b=2&b=3&c=3'))
+ fs = FieldStorage(fp=BytesIO(), environ={
+ 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'a=1&b=2&b=3&c=3'})
self.assertEqual(fs.getfirst('a'), '1')
self.assertEqual(fs.getfirst('b'), '2')
self.assertEqual(fs.getfirst('c'), '3')
@@ -30,8 +30,8 @@ def testPostRequestWithQuery(self):
self.assertEqual(fs.getlist('c'), ['3'])
def testPostRequestWithBody(self):
- fs = FieldStorage(fp=BytesIO(b'd=4&e=5&e=6&f=6'), environ=dict(
- REQUEST_METHOD='POST'))
+ fs = FieldStorage(fp=BytesIO(b'd=4&e=5&e=6&f=6'), environ={
+ 'REQUEST_METHOD': 'POST'})
self.assertEqual(fs.getfirst('d'), '4')
self.assertEqual(fs.getfirst('e'), '5')
self.assertEqual(fs.getfirst('f'), '6')
@@ -40,16 +40,16 @@ def testPostRequestWithBody(self):
self.assertEqual(fs.getlist('f'), ['6'])
def testPostRequestWithSpacesInValues(self):
- fs = FieldStorage(fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=b%20c+d'))
+ fs = FieldStorage(fp=BytesIO(), environ={
+ 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'a=b%20c+d'})
self.assertEqual(fs.getfirst('a'), 'b c d')
- fs = FieldStorage(fp=BytesIO(b'a=b%20c+d'), environ=dict(
- REQUEST_METHOD='POST'))
+ fs = FieldStorage(fp=BytesIO(b'a=b%20c+d'), environ={
+ 'REQUEST_METHOD': 'POST'})
self.assertEqual(fs.getfirst('a'), 'b c d')
def testPostRequestOverrides(self):
- fs = FieldStorage(fp=BytesIO(b'b=8&c=9&d=4&e=5&e=6&f=6'), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1&b=2&b=3&c=3'))
+ fs = FieldStorage(fp=BytesIO(b'b=8&c=9&d=4&e=5&e=6&f=6'), environ={
+ 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'a=1&b=2&b=3&c=3'})
self.assertEqual(fs.getfirst('a'), '1')
self.assertEqual(fs.getfirst('b'), '8')
self.assertEqual(fs.getfirst('c'), '9')
@@ -64,20 +64,23 @@ def testPostRequestOverrides(self):
self.assertEqual(fs.getlist('f'), ['6'])
def testPostRequestWithTooManyFields(self):
- fs = FieldStorage(fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1&a=2&a=3&a=4'),
+ fs = FieldStorage(
+ fp=BytesIO(),
+ environ={'REQUEST_METHOD': 'POST',
+ 'QUERY_STRING': 'a=1&a=2&a=3&a=4'},
max_num_fields=4)
self.assertEqual(fs.getlist('a'), ['1', '2', '3', '4'])
if hasattr(fs, 'max_num_fields'): # only test if this is supported
self.assertRaises(
ValueError, FieldStorage,
- fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1&a=2&a=3&a=4'),
+ fp=BytesIO(),
+ environ={'REQUEST_METHOD': 'POST',
+ 'QUERY_STRING': 'a=1&a=2&a=3&a=4'},
max_num_fields=3)
def testPostRequestWithQueryWithSemicolon1(self):
- fs = FieldStorage(fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1&b=2;b=3&c=3'))
+ fs = FieldStorage(fp=BytesIO(), environ={
+ 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'a=1&b=2;b=3&c=3'})
self.assertEqual(fs.getfirst('a'), '1')
self.assertEqual(fs.getfirst('c'), '3')
self.assertEqual(fs.getlist('a'), ['1'])
@@ -85,8 +88,10 @@ def testPostRequestWithQueryWithSemicolon1(self):
if hasSeparator(): # new Python version, splits only &
self.assertEqual(fs.getfirst('b'), '2;b=3')
self.assertEqual(fs.getlist('b'), ['2;b=3'])
- fs = FieldStorage(fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1&b=2&b=3&c=3'),
+ fs = FieldStorage(
+ fp=BytesIO(),
+ environ={'REQUEST_METHOD': 'POST',
+ 'QUERY_STRING': 'a=1&b=2&b=3&c=3'},
separator='&')
self.assertEqual(fs.getfirst('a'), '1')
self.assertEqual(fs.getfirst('b'), '2')
@@ -99,8 +104,8 @@ def testPostRequestWithQueryWithSemicolon1(self):
self.assertEqual(fs.getlist('b'), ['2', '3'])
def testPostRequestWithQueryWithSemicolon2(self):
- fs = FieldStorage(fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1;b=2&b=3;c=3'))
+ fs = FieldStorage(fp=BytesIO(), environ={
+ 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'a=1;b=2&b=3;c=3'})
if hasSeparator(): # new Python version, splits only &
self.assertEqual(fs.getfirst('a'), '1;b=2')
self.assertEqual(fs.getfirst('b'), '3;c=3')
@@ -108,8 +113,10 @@ def testPostRequestWithQueryWithSemicolon2(self):
self.assertEqual(fs.getlist('a'), ['1;b=2'])
self.assertEqual(fs.getlist('b'), ['3;c=3'])
self.assertEqual(fs.getlist('c'), [])
- fs = FieldStorage(fp=BytesIO(), environ=dict(
- REQUEST_METHOD='POST', QUERY_STRING='a=1;b=2;b=3;c=3'),
+ fs = FieldStorage(
+ fp=BytesIO(),
+ environ={'REQUEST_METHOD': 'POST',
+ 'QUERY_STRING': 'a=1;b=2;b=3;c=3'},
separator=';')
self.assertEqual(fs.getfirst('a'), '1')
self.assertEqual(fs.getfirst('b'), '2')
@@ -124,3 +131,150 @@ def testPostRequestWithQueryWithSemicolon2(self):
self.assertEqual(fs.getlist('a'), ['1'])
self.assertEqual(fs.getlist('b'), ['2', '3'])
self.assertEqual(fs.getlist('c'), ['3'])
+
+ def testPostRequestWithoutContentLength(self):
+ # see https://github.com/python/cpython/issues/71964
+ fs = FieldStorage(
+ fp=BytesIO(b'{"test":123}'),
+ environ={'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'application/json'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'application/json'})
+ self.assertEqual(fs.type, 'application/json')
+ self.assertEqual(fs.length, -1)
+ self.assertEqual(fs.bytes_read, 12)
+ assert fs.file.read() == '{"test":123}'
+
+ def testPostRequestWithContentLengthAndContentDispositionInline(self):
+ # see https://github.com/python/cpython/issues/71964
+ fs = FieldStorage(
+ fp=BytesIO(b'{"test":123}'),
+ headers={'content-length': 12, 'content-disposition': 'inline',
+ 'content-type': 'application/json'},
+ environ={'REQUEST_METHOD': 'POST'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'application/json', 'content-length': 12,
+ 'content-disposition': 'inline'})
+ self.assertEqual(fs.disposition, 'inline')
+ self.assertIsNone(fs.filename)
+ self.assertEqual(fs.type, 'application/json')
+ self.assertEqual(fs.length, 12)
+ self.assertEqual(fs.bytes_read, 12)
+ self.assertEqual(fs.file.read(), '{"test":123}')
+
+ def testPostRequestWithContentLengthAndContentDispositionAttachment(self):
+ # not affected by https://github.com/python/cpython/issues/71964
+ fs = FieldStorage(
+ fp=BytesIO(b'{"test":123}'),
+ headers={'content-length': 12,
+ 'content-disposition': 'attachment; filename="foo.json"',
+ 'content-type': 'application/json'},
+ environ={'REQUEST_METHOD': 'POST'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'application/json', 'content-length': 12,
+ 'content-disposition': 'attachment; filename="foo.json"'})
+ self.assertEqual(fs.disposition, 'attachment')
+ self.assertEqual(fs.filename, 'foo.json')
+ self.assertEqual(fs.type, 'application/json')
+ self.assertEqual(fs.length, 12)
+ self.assertEqual(fs.bytes_read, 12)
+ self.assertEqual(fs.file.read(), b'{"test":123}')
+
+ def testPostRequestWithContentLengthButWithoutContentDisposition(self):
+ # see https://github.com/python/cpython/issues/71964
+ fs = FieldStorage(fp=BytesIO(b'{"test":123}'), environ={
+ 'CONTENT_LENGTH': 12, 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'application/json'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'application/json', 'content-length': 12})
+ self.assertEqual(fs.disposition, '')
+ self.assertEqual(fs.type, 'application/json')
+ self.assertEqual(fs.length, 12)
+ self.assertEqual(fs.bytes_read, 12)
+ self.assertEqual(fs.file.read(), '{"test":123}')
+
+ def testPostRequestWithUtf8BinaryData(self):
+ text = 'The \u2603 by Raymond Briggs'
+ content = text.encode('utf-8')
+ length = len(content)
+ fs = FieldStorage(fp=BytesIO(content), environ={
+ 'CONTENT_LENGTH': length, 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'application/octet-stream'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'application/octet-stream',
+ 'content-length': length})
+ self.assertEqual(fs.type, 'application/octet-stream')
+ self.assertEqual(fs.length, length)
+ self.assertEqual(fs.bytes_read, length)
+ self.assertEqual(fs.file.read(), content)
+
+ def testPostRequestWithNonUtf8BinaryData(self):
+ # see https://github.com/WebwareForPython/w4py3/issues/14
+ content = b'\xfe\xff\xc0'
+ with self.assertRaises(UnicodeDecodeError):
+ content.decode('utf-8')
+ length = len(content)
+ fs = FieldStorage(fp=BytesIO(content), environ={
+ 'CONTENT_LENGTH': length, 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'application/octet-stream'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'application/octet-stream',
+ 'content-length': length})
+ self.assertEqual(fs.type, 'application/octet-stream')
+ self.assertEqual(fs.length, length)
+ self.assertEqual(fs.bytes_read, length)
+ self.assertEqual(fs.file.read(), content)
+
+ def testPostRequestWithUtf8TextData(self):
+ text = 'The \u2603 by Raymond Briggs'
+ content = text.encode('utf-8')
+ length = len(content)
+ fs = FieldStorage(fp=BytesIO(content), environ={
+ 'CONTENT_LENGTH': length, 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'text/plain'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'text/plain',
+ 'content-length': length})
+ self.assertEqual(fs.type, 'text/plain')
+ self.assertEqual(fs.length, length)
+ self.assertEqual(fs.bytes_read, length)
+ self.assertEqual(fs.file.read(), text)
+
+ def testPostRequestWithNonUtf8TextData(self):
+ # see https://github.com/WebwareForPython/w4py3/issues/14
+ content = b'\xfe\xff\xc0'
+ with self.assertRaises(UnicodeDecodeError):
+ content.decode('utf-8')
+ length = len(content)
+ fs = FieldStorage(fp=BytesIO(content), environ={
+ 'CONTENT_LENGTH': length, 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'text/plain'})
+ self.assertEqual(fs.headers, {
+ 'content-type': 'text/plain',
+ 'content-length': length})
+ self.assertEqual(fs.type, 'text/plain')
+ self.assertEqual(fs.length, length)
+ self.assertEqual(fs.bytes_read, length)
+ self.assertEqual(fs.file.read(), content)
+
+ def testIsBinaryType(self):
+ self.assertIs(isBinaryType('application/json'), False)
+ self.assertIs(isBinaryType('application/xml'), False)
+ self.assertIs(isBinaryType('application/calendar+json'), False)
+ self.assertIs(isBinaryType('application/calendar+xml'), False)
+ self.assertIs(isBinaryType('model/x3d+xml'), False)
+ self.assertIs(isBinaryType('text/csv'), False)
+ self.assertIs(isBinaryType('text/html'), False)
+ self.assertIs(isBinaryType('text/plain'), False)
+ self.assertIs(isBinaryType('x3d+xml'), False)
+ self.assertIs(isBinaryType('application/octet-stream'), True)
+ self.assertIs(isBinaryType('application/pdf'), True)
+ self.assertIs(isBinaryType('application/zip'), True)
+ self.assertIs(isBinaryType('audio/ogg'), True)
+ self.assertIs(isBinaryType('font/otf'), True)
+ self.assertIs(isBinaryType('image/png'), True)
+ self.assertIs(isBinaryType('video/mp4'), True)
+ self.assertIs(isBinaryType('application/json',
+ {'charset': 'utf8'}), False)
+ self.assertIs(isBinaryType('text/csv',
+ {'charset': 'binary'}), True)
diff --git a/webware/WebUtils/Tests/TestFieldStorageStandard.py b/webware/WebUtils/Tests/TestFieldStorageStandard.py
index 229e0d6..22f5ee2 100644
--- a/webware/WebUtils/Tests/TestFieldStorageStandard.py
+++ b/webware/WebUtils/Tests/TestFieldStorageStandard.py
@@ -17,7 +17,7 @@ class HackedSysModule:
cgi.sys = HackedSysModule()
-def genTesult(data, environ):
+def genResult(data, environ):
encoding = 'latin-1'
fakeStdin = BytesIO(data.encode(encoding))
fakeStdin.seek(0)
@@ -172,7 +172,7 @@ def check(content):
'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
'REQUEST_METHOD': 'POST',
}
- self.assertEqual(genTesult(data, environ),
+ self.assertEqual(genResult(data, environ),
{'upload': content.encode('latin1')})
check('x' * (maxline - 1))
check('x' * (maxline - 1) + '\r')
@@ -271,7 +271,7 @@ def testQSAndUrlEncode(self):
'QUERY_STRING': 'key1=value1&key2=value2y',
'REQUEST_METHOD': 'POST',
}
- v = genTesult(data, environ)
+ v = genResult(data, environ)
self.assertNotEqual(self._qsResult, v)
self.assertEqual(self._qsResultModified, v)
@@ -344,7 +344,7 @@ def testQSAndFormData(self):
'QUERY_STRING': 'key1=value1&key2=value2x',
'REQUEST_METHOD': 'POST',
}
- v = genTesult(data, environ)
+ v = genResult(data, environ)
self.assertEqual(self._qsResult, v)
def testQSAndFormDataFile(self):
@@ -376,7 +376,7 @@ def testQSAndFormDataFile(self):
}
result = self._qsResult.copy()
result['upload'] = b'this is the content of the fake file\n'
- v = genTesult(data, environ)
+ v = genResult(data, environ)
self.assertEqual(result, v)
def testParseHeader(self):
diff --git a/webware/WebUtils/Tests/TestFuncs.py b/webware/WebUtils/Tests/TestFuncs.py
index cd5629e..dfe1ef4 100644
--- a/webware/WebUtils/Tests/TestFuncs.py
+++ b/webware/WebUtils/Tests/TestFuncs.py
@@ -61,33 +61,33 @@ def testUrlRoundTrip(self):
def testHtmlDorDict(self):
f = Funcs.htmlForDict
self.assertEqual(
- f(dict(foo='bar', answer=42)),
+ f({'foo': 'bar', 'answer': 42}),
'\n'
'| answer | 42 |
\n'
'| foo | bar |
\n
')
self.assertEqual(
- f(dict(foo='ba,zong', bar='ka;woom'),
- addSpace=dict(foo=',', bar=';')),
+ f({'foo': 'ba,zong', 'bar': 'ka;woom'},
+ addSpace={'foo': ',', 'bar': ';'}),
'\n'
'| bar | ka; woom |
\n'
'| foo | ba, zong |
\n
')
self.assertEqual(
- f(dict(foo='barbarabarbarabarbarabarbara'), maxValueLength=12),
+ f({'foo': 'barbarabarbarabarbarabarbara'}, maxValueLength=12),
'')
self.assertEqual(
- f(dict(foo='zing', bar='zang'),
+ f({'foo': 'zing', 'bar': 'zang'},
filterValueCallBack=lambda v, k, d: 'zung' if k == 'bar' else v),
'\n'
'| bar | zung |
\n'
'| foo | zing |
\n
')
self.assertEqual(
- f(dict(foo='bar'), topHeading='twinkle'),
+ f({'foo': 'bar'}, topHeading='twinkle'),
'\n'
'| twinkle |
\n'
'| foo | bar |
\n
')
self.assertEqual(
- f(dict(foo='bar'), topHeading=('key', 'value')),
+ f({'foo': 'bar'}, topHeading=('key', 'value')),
'\n'
'| key | value |
\n'
'| foo | bar |
\n
')
@@ -103,12 +103,12 @@ def testHtmlDorDict(self):
def testRequestURI(self):
f = Funcs.requestURI
self.assertEqual(
- f(dict(REQUEST_URI='http://w4py.org')), 'http://w4py.org')
+ f({'REQUEST_URI': 'http://w4py.org'}), 'http://w4py.org')
self.assertEqual(
- f(dict(SCRIPT_URL='http://w4py.org', QUERY_STRING='foo=bar')),
+ f({'SCRIPT_URL': 'http://w4py.org', 'QUERY_STRING': 'foo=bar'}),
'http://w4py.org?foo=bar')
self.assertEqual(
- f(dict(SCRIPT_NAME='/test', QUERY_STRING='foo=bar')),
+ f({'SCRIPT_NAME': '/test', 'QUERY_STRING': 'foo=bar'}),
'/test?foo=bar')
def testNormURL(self):