Index: validate.py =================================================================== --- validate.py (revision 73702) +++ validate.py (working copy) @@ -1,7 +1,9 @@ -# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) -# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php -# Also licenced under the Apache License, 2.0: http://opensource.org/licenses/apache2.0.php +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php Also licenced under +# the Apache License, 2.0: http://opensource.org/licenses/apache2.0.php # Licensed to PSF under a Contributor Agreement + """ Middleware to check for obedience to the WSGI specification. @@ -135,7 +137,6 @@ return str(value, "iso-8859-1") def validator(application): - """ When applied between a WSGI server and a WSGI application, this middleware will check for WSGI compliancy on a number of levels. @@ -189,6 +190,7 @@ return lint_app + class InputWrapper: def __init__(self, wsgi_input): @@ -223,6 +225,7 @@ def close(self): assert_(0, "input.close() must not be called") + class ErrorWrapper: def __init__(self, wsgi_errors): @@ -242,6 +245,7 @@ def close(self): assert_(0, "errors.close() must not be called") + class WriteWrapper: def __init__(self, wsgi_writer): @@ -251,6 +255,7 @@ assert_(isinstance(s, (str, bytes))) self.writer(s) + class PartialIteratorWrapper: def __init__(self, wsgi_iterator): @@ -260,6 +265,7 @@ # We want to make sure __iter__ is called return IteratorWrapper(self.iterator, None) + class IteratorWrapper: def __init__(self, wsgi_iterator, check_start_response): @@ -276,8 +282,9 @@ "Iterator read after closed") v = next(self.iterator) if self.check_start_response is not None: - assert_(self.check_start_response, - "The application returns and we started iterating over its body, but start_response has not yet been called") + msg = ("The application returns and we started iterating over its " + "body, but start_response has not yet been called") + assert_(self.check_start_response, msg) self.check_start_response = None return v @@ -293,6 +300,7 @@ assert_(self.closed, "Iterator garbage collected without being closed") + def check_environ(environ): assert_(isinstance(environ, dict), "Environment is not of the right type: %r (environment: %r)" @@ -358,18 +366,21 @@ "SCRIPT_NAME cannot be '/'; it should instead be '', and " "PATH_INFO should be '/'") + def check_input(wsgi_input): for attr in ['read', 'readline', 'readlines', '__iter__']: assert_(hasattr(wsgi_input, attr), "wsgi.input (%r) doesn't have the attribute %s" % (wsgi_input, attr)) + def check_errors(wsgi_errors): for attr in ['flush', 'write', 'writelines']: assert_(hasattr(wsgi_errors, attr), "wsgi.errors (%r) doesn't have the attribute %s" % (wsgi_errors, attr)) + def check_status(status): status = check_string_type(status, "Status") # Implicitly check that we can turn it into an integer: @@ -384,6 +395,7 @@ "followed by a single space and a status explanation" % status, WSGIWarning) + def check_headers(headers): assert_(isinstance(headers, list), "Headers (%r) must be of type list: %r" @@ -411,6 +423,7 @@ assert_(0, "Bad header value: %r (bad char: %r)" % (value, bad_header_value_re.search(value).group(0))) + def check_content_type(status, headers): status = check_string_type(status, "Status") code = int(status.split(None, 1)[0]) @@ -427,11 +440,13 @@ if code not in NO_MESSAGE_BODY: assert_(0, "No Content-Type header found in headers (%s)" % headers) + def check_exc_info(exc_info): assert_(exc_info is None or isinstance(exc_info, tuple), "exc_info (%r) is not a tuple: %r" % (exc_info, type(exc_info))) # More exc_info checks? + def check_iterator(iterator): # Technically a string is legal, which is why it's a really bad # idea, because it may cause the response to be returned Index: headers.py =================================================================== --- headers.py (revision 73702) +++ headers.py (working copy) @@ -25,23 +25,10 @@ return param - - - - - - - - - - - - class Headers: + """Manage a collection of HTTP response headers.""" - """Manage a collection of HTTP response headers""" - - def __init__(self,headers): + def __init__(self, headers): if not isinstance(headers, list): raise TypeError("Headers must be a list of name/value tuples") self._headers = [] @@ -68,7 +55,7 @@ self._headers.append( (self._convert_string_type(name), self._convert_string_type(val))) - def __delitem__(self,name): + def __delitem__(self, name): """Delete all occurrences of a header, if present. Does *not* raise an exception if the header is missing. @@ -76,7 +63,7 @@ name = self._convert_string_type(name.lower()) self._headers[:] = [kv for kv in self._headers if kv[0].lower() != name] - def __getitem__(self,name): + def __getitem__(self, name): """Get the first header value for 'name' Return None if the header is missing instead of raising an exception. @@ -87,15 +74,10 @@ """ return self.get(name) - - - - def __contains__(self, name): """Return true if the message contains the header.""" return self.get(name) is not None - def get_all(self, name): """Return a list of all the values for the named field. @@ -107,16 +89,14 @@ name = self._convert_string_type(name.lower()) return [kv[1] for kv in self._headers if kv[0].lower()==name] - - def get(self,name,default=None): + def get(self, name, default=None): """Get the first header value for 'name', or return 'default'""" name = self._convert_string_type(name.lower()) - for k,v in self._headers: + for k, v in self._headers: if k.lower()==name: return v return default - def keys(self): """Return a list of all the header field names. @@ -125,11 +105,8 @@ Any fields deleted and re-inserted are always appended to the header list. """ - return [k for k, v in self._headers] + return [h[0] for h in self._headers] - - - def values(self): """Return a list of all header values. @@ -138,7 +115,7 @@ Any fields deleted and re-inserted are always appended to the header list. """ - return [v for k, v in self._headers] + return [h[1] for h in self._headers] def items(self): """Get all the header fields and values. @@ -156,9 +133,9 @@ def __str__(self): """str() returns the formatted headers, complete with end line, suitable for direct HTTP transmission.""" - return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['','']) + return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['', '']) - def setdefault(self,name,value): + def setdefault(self, name, value): """Return first matching header value for 'name', or 'value' If there is no header named 'name', add a new header with name 'name' @@ -171,7 +148,6 @@ else: return result - def add_header(self, _name, _value, **_params): """Extended header setting. @@ -199,20 +175,5 @@ else: v = self._convert_string_type(v) parts.append(_formatparam(k.replace('_', '-'), v)) - self._headers.append((self._convert_string_type(_name), "; ".join(parts))) - - - - - - - - - - - - - - - -# + self._headers.append((self._convert_string_type(_name), + "; ".join(parts))) Index: util.py =================================================================== --- util.py (revision 73702) +++ util.py (working copy) @@ -9,7 +9,7 @@ class FileWrapper: - """Wrapper to convert file-like objects to iterables""" + """Wrapper to convert file-like objects to iterables.""" def __init__(self, filelike, blksize=8192): self.filelike = filelike @@ -17,7 +17,7 @@ if hasattr(filelike,'close'): self.close = filelike.close - def __getitem__(self,key): + def __getitem__(self, key): data = self.filelike.read(self.blksize) if data: return data @@ -33,20 +33,14 @@ raise StopIteration - - - - - - def guess_scheme(environ): - """Return a guess for whether 'wsgi.url_scheme' should be 'http' or 'https' - """ - if environ.get("HTTPS") in ('yes','on','1'): + """Return a guess for whether 'wsgi.url_scheme' should be 'http' or 'https'""" + if environ.get("HTTPS") in ('yes', 'on', '1'): return 'https' else: return 'http' + def application_uri(environ): """Return the application's base URI (no PATH_INFO or QUERY_STRING)""" url = environ['wsgi.url_scheme']+'://' @@ -67,6 +61,7 @@ url += quote(environ.get('SCRIPT_NAME') or '/') return url + def request_uri(environ, include_query=1): """Return the full request URI, optionally including the query string""" url = application_uri(environ) @@ -80,6 +75,7 @@ url += '?' + environ['QUERY_STRING'] return url + def shift_path_info(environ): """Shift a name from PATH_INFO to SCRIPT_NAME, returning it @@ -117,10 +113,11 @@ # if there's only one path part left. Instead of fixing this # above, we fix it here so that PATH_INFO gets normalized to # an empty string in the environ. - if name=='.': + if name == '.': name = None return name + def setup_testing_defaults(environ): """Update 'environ' with trivial defaults for testing purposes @@ -137,14 +134,14 @@ environ.setdefault('SERVER_NAME','127.0.0.1') environ.setdefault('SERVER_PROTOCOL','HTTP/1.0') - environ.setdefault('HTTP_HOST',environ['SERVER_NAME']) + environ.setdefault('HTTP_HOST', environ['SERVER_NAME']) environ.setdefault('REQUEST_METHOD','GET') if 'SCRIPT_NAME' not in environ and 'PATH_INFO' not in environ: environ.setdefault('SCRIPT_NAME','') environ.setdefault('PATH_INFO','/') - environ.setdefault('wsgi.version', (1,0)) + environ.setdefault('wsgi.version', (1, 0)) environ.setdefault('wsgi.run_once', 0) environ.setdefault('wsgi.multithread', 0) environ.setdefault('wsgi.multiprocess', 0) @@ -152,54 +149,21 @@ from io import StringIO, BytesIO environ.setdefault('wsgi.input', BytesIO()) environ.setdefault('wsgi.errors', StringIO()) - environ.setdefault('wsgi.url_scheme',guess_scheme(environ)) + environ.setdefault('wsgi.url_scheme', guess_scheme(environ)) - if environ['wsgi.url_scheme']=='http': + if environ['wsgi.url_scheme'] == 'http': environ.setdefault('SERVER_PORT', '80') - elif environ['wsgi.url_scheme']=='https': + elif environ['wsgi.url_scheme'] == 'https': environ.setdefault('SERVER_PORT', '443') - - _hoppish = { 'connection':1, 'keep-alive':1, 'proxy-authenticate':1, 'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1, 'upgrade':1 }.__contains__ + def is_hop_by_hop(header_name): """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header""" return _hoppish(header_name.lower()) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# Index: simple_server.py =================================================================== --- simple_server.py (revision 73702) +++ simple_server.py (working copy) @@ -31,19 +31,15 @@ def close(self): try: self.request_handler.log_request( - self.status.split(' ',1)[0], self.bytes_sent + self.status.split(' ', 1)[0], self.bytes_sent ) finally: SimpleHandler.close(self) - - - class WSGIServer(HTTPServer): + """BaseHTTPServer that implements the Python WSGI protocol.""" - """BaseHTTPServer that implements the Python WSGI protocol""" - application = None def server_bind(self): @@ -52,7 +48,7 @@ self.setup_environ() def setup_environ(self): - # Set up base environment + """Set up base environment.""" env = self.base_environ = {} env['SERVER_NAME'] = self.server_name env['GATEWAY_INTERFACE'] = 'CGI/1.1' @@ -64,23 +60,10 @@ def get_app(self): return self.application - def set_app(self,application): + def set_app(self, application): self.application = application - - - - - - - - - - - - - class WSGIRequestHandler(BaseHTTPRequestHandler): server_version = "WSGIServer/" + __version__ @@ -90,9 +73,9 @@ env['SERVER_PROTOCOL'] = self.request_version env['REQUEST_METHOD'] = self.command if '?' in self.path: - path,query = self.path.split('?',1) + path, query = self.path.split('?', 1) else: - path,query = self.path,'' + path, query = self.path, '' env['PATH_INFO'] = urllib.parse.unquote(path) env['QUERY_STRING'] = query @@ -112,13 +95,14 @@ env['CONTENT_LENGTH'] = length for k, v in self.headers.items(): - k=k.replace('-','_').upper(); v=v.strip() + k = k.replace('-', '_').upper() + v = v.strip() if k in env: continue # skip content length, type,etc. - if 'HTTP_'+k in env: - env['HTTP_'+k] += ','+v # comma-separate multiple headers + if 'HTTP_' + k in env: + env['HTTP_' + k] += ',' + v # comma-separate multiple headers else: - env['HTTP_'+k] = v + env['HTTP_' + k] = v return env def get_stderr(self): @@ -138,45 +122,20 @@ handler.run(self.server.get_app()) - - - - - - - - - - - - - - - - - - - - - - - - -def demo_app(environ,start_response): +def demo_app(environ, start_response): from io import StringIO stdout = StringIO() print("Hello world!", file=stdout) print(file=stdout) h = sorted(environ.items()) - for k,v in h: - print(k,'=',repr(v), file=stdout) - start_response(b"200 OK", [(b'Content-Type',b'text/plain; charset=utf-8')]) + for k, v in h: + print(k, '=', repr(v), file=stdout) + start_response(b"200 OK", [(b'Content-Type', b'text/plain; charset=utf-8')]) return [stdout.getvalue().encode("utf-8")] -def make_server( - host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler -): +def make_server(host, port, app, server_class=WSGIServer, + handler_class=WSGIRequestHandler): """Create a new WSGI server listening on `host` and `port` for `app`""" server = server_class((host, port), handler_class) server.set_app(app) @@ -190,16 +149,3 @@ import webbrowser webbrowser.open('http://localhost:8000/xyz?abc') httpd.handle_request() # serve one request, then exit - - - - - - - - - - - - -# Index: handlers.py =================================================================== --- handlers.py (revision 73702) +++ handlers.py (working copy) @@ -14,18 +14,18 @@ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] def format_date_time(timestamp): + """Transform days and months from number to string.""" year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( _weekdayname[wd], day, _monthname[month], year, hh, mm, ss ) - class BaseHandler: """Manage the invocation of a WSGI application""" # Configuration parameters; can override per-subclass or per-instance - wsgi_version = (1,0) + wsgi_version = (1, 0) wsgi_multithread = True wsgi_multiprocess = True wsgi_run_once = False @@ -55,15 +55,8 @@ headers = None bytes_sent = 0 - - - - - - - def run(self, application): - """Invoke the application""" + """Invoke the application.""" # Note to self: don't move the close()! Asynchronous servers shouldn't # call close() from finish_response(), so if you close() anywhere but # the double-error branch here, you'll break asynchronous servers by @@ -79,9 +72,9 @@ except: # If we get an error handling an error, just give up already! self.close() - raise # ...and let the actual server figure it out. + # ...and let the actual server figure it out. + raise - def setup_environ(self): """Set up the environment for one request""" @@ -100,11 +93,10 @@ env['wsgi.file_wrapper'] = self.wsgi_file_wrapper if self.origin_server and self.server_software: - env.setdefault('SERVER_SOFTWARE',self.server_software) + env.setdefault('SERVER_SOFTWARE', self.server_software) - def finish_response(self): - """Send any iterable data, then close self and the iterable + """Send any iterable data, then close self and the iterable. Subclasses intended for use in asynchronous servers will want to redefine this method, such that it sets up callbacks @@ -117,12 +109,10 @@ self.finish_content() self.close() - def get_scheme(self): - """Return the URL scheme being used""" + """Return the URL scheme being used.""" return guess_scheme(self.environ) - def set_content_length(self): """Compute Content-Length or switch to chunked encoding if possible""" try: @@ -130,23 +120,21 @@ except (TypeError,AttributeError,NotImplementedError): pass else: - if blocks==1: + if blocks == 1: self.headers['Content-Length'] = str(self.bytes_sent) return # XXX Try for chunked encoding if origin server and client is 1.1 - def cleanup_headers(self): - """Make any necessary header changes or defaults + """Make any necessary header changes or defaults. Subclasses can extend this to add other defaults. """ if 'Content-Length' not in self.headers: self.set_content_length() - def start_response(self, status, headers,exc_info=None): + def start_response(self, status, headers, exc_info=None): """'start_response()' callable as specified by PEP 333""" - if exc_info: try: if self.headers_sent: @@ -158,12 +146,12 @@ raise AssertionError("Headers already set!") status = self._convert_string_type(status, "Status") - assert len(status)>=4,"Status must be at least 4 characters" - assert int(status[:3]),"Status message must begin w/3-digit code" - assert status[3]==" ", "Status message must have a space after code" + assert len(status) >= 4, "Status must be at least 4 characters" + assert int(status[:3]), "Status message must begin w/3-digit code" + assert status[3] == " ", "Status message must have a space after code" str_headers = [] - for name,val in headers: + for name, val in headers: name = self._convert_string_type(name, "Header name") val = self._convert_string_type(val, "Header value") str_headers.append((name, val)) @@ -178,14 +166,14 @@ if isinstance(value, str): return value assert isinstance(value, bytes), \ - "{0} must be a string or bytes object (not {1})".format(title, value) + "{0} must be a string or bytes object, not {1}".format(title, value) return str(value, "iso-8859-1") def send_preamble(self): - """Transmit version/status/date/server, via self._write()""" + """Transmit version/status/date/server, via self._write().""" if self.origin_server: if self.client_is_modern(): - self._write('HTTP/%s %s\r\n' % (self.http_version,self.status)) + self._write('HTTP/%s %s\r\n' % (self.http_version, self.status)) if 'Date' not in self.headers: self._write( 'Date: %s\r\n' % format_date_time(time.time()) @@ -196,10 +184,10 @@ self._write('Status: %s\r\n' % self.status) def write(self, data): - """'write()' callable as specified by PEP 333""" + """'write()' callable as specified by PEP 333.""" assert isinstance(data, (str, bytes)), \ - "write() argument must be a string or bytes" + "write() argument must be a string or bytes object" if not self.status: raise AssertionError("write() before start_response()") @@ -215,9 +203,8 @@ self._write(data) self._flush() - def sendfile(self): - """Platform-specific file transmission + """Platform-specific file transmission. Override this method in subclasses to support platform-specific file transmission. It is only called if the application's @@ -236,9 +223,8 @@ """ return False # No platform-specific transmission by default - def finish_content(self): - """Ensure headers and content have both been sent""" + """Ensure headers and content have both been sent.""" if not self.headers_sent: self.headers['Content-Length'] = "0" self.send_headers() @@ -246,7 +232,7 @@ pass # XXX check if content-length was too short? def close(self): - """Close the iterable (if needed) and reset all instance vars + """Close the iterable (if needed) and reset all instance vars. Subclasses may want to also drop the client connection. """ @@ -255,32 +241,29 @@ self.result.close() finally: self.result = self.headers = self.status = self.environ = None - self.bytes_sent = 0; self.headers_sent = False + self.bytes_sent = 0 + self.headers_sent = False - def send_headers(self): - """Transmit headers to the client, via self._write()""" + """Transmit headers to the client, via self._write().""" self.cleanup_headers() self.headers_sent = True if not self.origin_server or self.client_is_modern(): self.send_preamble() self._write(str(self.headers)) - def result_is_file(self): - """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'""" + """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'.""" wrapper = self.wsgi_file_wrapper - return wrapper is not None and isinstance(self.result,wrapper) + return wrapper is not None and isinstance(self.result, wrapper) - def client_is_modern(self): - """True if client can accept status and headers""" + """True if client can accept status and headers.""" return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9' + def log_exception(self, exc_info): + """Log the 'exc_info' tuple in the server log. - def log_exception(self,exc_info): - """Log the 'exc_info' tuple in the server log - Subclasses may override to retarget the output or change its format. """ try: @@ -295,7 +278,7 @@ exc_info = None def handle_error(self): - """Log current error, and send error output to client if possible""" + """Log current error, and send error output to client if possible.""" self.log_exception(sys.exc_info()) if not self.headers_sent: self.result = self.error_output(self.environ, self.start_response) @@ -303,7 +286,7 @@ # XXX else: attempt advanced recovery techniques for HTML or text? def error_output(self, environ, start_response): - """WSGI mini-app to create error output + """WSGI mini-app to create error output. By default, this just uses the 'error_status', 'error_headers', and 'error_body' attributes to generate an output page. It can @@ -315,15 +298,14 @@ something special to enable diagnostic output, which is why we don't include any here! """ - start_response(self.error_status,self.error_headers[:],sys.exc_info()) + start_response(self.error_status, self.error_headers[:], sys.exc_info()) return [self.error_body] # Pure abstract methods; *must* be overridden in subclasses + def _write(self, data): + """Override in subclass to buffer data for send to client. - def _write(self,data): - """Override in subclass to buffer data for send to client - It's okay if this method actually transmits the data; BaseHandler just separates write and flush operations for greater efficiency when the underlying system actually has such a distinction. @@ -331,7 +313,7 @@ raise NotImplementedError def _flush(self): - """Override in subclass to force sending of recent '_write()' calls + """Override in subclass to force sending of recent '_write()' calls. It's okay if this method is a no-op (i.e., if '_write()' actually sends the data. @@ -351,15 +333,6 @@ raise NotImplementedError - - - - - - - - - class SimpleHandler(BaseHandler): """Handler that's just initialized with streams, environment, etc. @@ -373,9 +346,8 @@ ) handler.run(app)""" - def __init__(self,stdin,stdout,stderr,environ, - multithread=True, multiprocess=False - ): + def __init__(self, stdin, stdout, stderr, environ, multithread=True, + multiprocess=False): self.stdin = stdin self.stdout = stdout self.stderr = stderr @@ -392,7 +364,7 @@ def add_cgi_vars(self): self.environ.update(self.base_env) - def _write(self,data): + def _write(self, data): if isinstance(data, str): try: data = data.encode("iso-8859-1") @@ -407,11 +379,10 @@ class BaseCGIHandler(SimpleHandler): + """CGI-like systems using input/output/error streams and environ mapping. - """CGI-like systems using input/output/error streams and environ mapping + Usage: - Usage:: - handler = BaseCGIHandler(inp,out,err,env) handler.run(app) @@ -430,29 +401,11 @@ origin_server = False - - - - - - - - - - - - - - - - - class CGIHandler(BaseCGIHandler): + """CGI-based invocation via sys.stdin/stdout/stderr and os.environ. - """CGI-based invocation via sys.stdin/stdout/stderr and os.environ + Usage: - Usage:: - CGIHandler().run(app) The difference between this class and BaseCGIHandler is that it always @@ -471,20 +424,3 @@ self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()), multithread=False, multiprocess=True ) - - - - - - - - - - - - - - - - -#