From aa255a3981ca91d9e01865d456249c4339f45261 Mon Sep 17 00:00:00 2001 From: Ondrej Tuma Date: Sun, 1 Mar 2026 08:17:43 +0100 Subject: [PATCH] English corrections --- ARCHITECTURE.rst | 4 +- CONTRIBUTION.rst | 2 +- README.rst | 6 +- doc/about.rst | 90 +-- doc/documentation.rst | 713 ++++++++++++------------ examples/http_digest.py | 30 +- examples/large_file.py | 51 +- examples/metrics.py | 14 +- examples/openapi3.py | 36 +- examples/put_file.py | 12 +- examples/simple.py | 81 +-- examples/simple_json.py | 20 +- examples/websocket.py | 28 +- poorwsgi/__init__.py | 24 +- poorwsgi/digest.py | 49 +- poorwsgi/fieldstorage.py | 289 +++++----- poorwsgi/headers.py | 121 ++-- poorwsgi/openapi_wrapper.py | 39 +- poorwsgi/request.py | 352 ++++++------ poorwsgi/response.py | 712 ++++++++++++++---------- poorwsgi/results.py | 516 ++++++++++-------- poorwsgi/session.py | 139 ++--- poorwsgi/state.py | 7 +- poorwsgi/wsgi.py | 880 +++++++++++++++++------------- setup.py | 1 - tests/test_application_error.py | 9 +- tests/test_digest.py | 9 +- tests/test_header.py | 82 +-- tests/test_request.py | 50 +- tests/test_responses.py | 432 +++++++++------ tests/test_route_validation.py | 235 ++++---- tests/test_session.py | 58 +- tests_integrity/conftest.py | 4 +- tests_integrity/openapi.py | 42 +- tests_integrity/support.py | 8 +- tests_integrity/test_digest.py | 17 +- tests_integrity/test_json.py | 22 +- tests_integrity/test_metrics.py | 8 +- tests_integrity/test_openapi.py | 26 +- tests_integrity/test_profile.py | 6 +- tests_integrity/test_simple.py | 58 +- tests_integrity/test_websocket.py | 9 +- 42 files changed, 2977 insertions(+), 2314 deletions(-) diff --git a/ARCHITECTURE.rst b/ARCHITECTURE.rst index cc8ffbd..2cb0e8b 100644 --- a/ARCHITECTURE.rst +++ b/ARCHITECTURE.rst @@ -49,10 +49,10 @@ This module defines how outgoing responses are constructed. `results` (`poorwsgi/results.py`) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This module contains pre-defined handlers for all standard HTTP status codes (e.g., `not_found` for 404, `internal_server_error` for 500). +This module contains predefined handlers for all standard HTTP status codes (e.g., `not_found` for 404, `internal_server_error` for 500). * These are used as default error handlers if the user does not register their own. -* It also contains the powerful `debug_info` handler, which generates a comprehensive introspection page at the `/_debug-info` URL when the application is in debug mode. +* It also contains the powerful `debug_info` handler, which generates a comprehensive introspection page at the `/debug-info` URL when the application is in debug mode. The Request-Response Lifecycle ------------------------------ diff --git a/CONTRIBUTION.rst b/CONTRIBUTION.rst index ca8e850..eca3330 100644 --- a/CONTRIBUTION.rst +++ b/CONTRIBUTION.rst @@ -7,7 +7,7 @@ Reporting Bugs -------------- If you encounter a bug, please ensure that you: -1. Search the existing `issues `_ to make sure the bug has not already been reported. +1. Search the existing `issues `_ to ensure the bug has not already been reported. 2. If you cannot find the bug, create a new issue. 3. In the description, provide as much information as possible: diff --git a/README.rst b/README.rst index e3a19c6..9cdd054 100644 --- a/README.rst +++ b/README.rst @@ -25,8 +25,8 @@ Poor WSGI for Python ==================== -Poor WSGI for Python is light WGI connector with uri routing between WSGI server -and your application. The simplest way to run and test it looks like that: +Poor WSGI for Python is a lightweight WSGI connector with URI routing between the WSGI server +and your application. The simplest way to run and test it looks like this: .. code-block:: python @@ -43,7 +43,7 @@ and your application. The simplest way to run and test it looks like that: httpd = make_server('127.0.0.1', 8080, app) httpd.serve_forever() -You can use python wsgiref.simple_server for test it: +You can use Python's wsgiref.simple_server to test it: .. code-block:: diff --git a/doc/about.rst b/doc/about.rst index c62c4d5..9ecd72b 100644 --- a/doc/about.rst +++ b/doc/about.rst @@ -1,9 +1,9 @@ About PoorWSGI ============== -PoorWSGI for Python is light WGI connector with uri routing between WSGI -server and your application. It have request object like in mod_python, -which is post to all uri or http state handlers. The simplest way to run and -test with wsgiref.simple_server it looks like that: +PoorWSGI for Python is a lightweight WSGI connector with URI routing between the WSGI +server and your application. It has a request object similar to mod_python, +which is passed to all URI or HTTP state handlers. The simplest way to run and +test it with wsgiref.simple_server looks like this: .. code:: python @@ -24,17 +24,17 @@ test with wsgiref.simple_server it looks like that: ~$ python simple.py -It has base error pages like 403, 404, 405, 500 or 501. 500 internal server -error have debug output if poor_Debug is set. And there is special debug page -on ``/debug-info`` uri, which is available when poor_Debug is set too. +It has basic error pages like 403, 404, 405, 500, or 501. A 500 internal server +error will have debug output if poor_Debug is set. Additionally, there is a special debug page +on the ``/debug-info`` URI, which is also available when poor_Debug is set. .. code:: sh ~$ poor_Debug=On python simple.py -Poor WSGI have some functions, to you can use as real http server, which could -send files with right mime-type from disk, or generate directory listing. See -Configuration section for more info. +PoorWSGI has some functions that you can use as a real HTTP server, which can +send files with the correct MIME type from disk, or generate directory listings. +See the Configuration section for more info. .. code:: sh @@ -42,50 +42,50 @@ Configuration section for more info. The Story ========= -Once upon a time, there was a King. Ok there was a Prince. Oh, may by, there -was not a prince, but probably, there was a Programmer, hmm ok, programmer. -And this programmer know apaches mod_python. Yes it was very very bad paragon, -but before python, he was programing in php. So mod_python was be big movement -to right direction at that times. - -He was founding how he can write, and host on server python applications. And as -he know some close-source framework, which works right, he write some another, -similar for his use. That is base of Poor Publisher. But WGSI was coming so he -had idea, to write some new backend for his applications. That is base of Poor -HTTP and Poor WSGI. - -Some times, Poor HTTP and Poor WSGI was one project. It is better way, but -that's not right way. After some time, he divide these too projects to Poor WSGI -and Poor HTTP projects. But there is bad concept in Poor WSGI framework, which -is not framework in fact. So he look for another projects, and see how could be -nice to create WSGI application for user. That is time when Poor WSGI is -rewritten to library type code, and application is callable class with some nice -route and other methods - decorators. - -This is story of one programmer and his WSGI framework, which is not framework -in fact, because, it knows only handle uri request with some mod_python -compatibility layer. As you can see, there are some ways, how this project can -go. It's author, programmer use it on his projects, and it would be so nice, if -there are more programmers then he, which use this little project, let's call -it WSGI connector. +Once upon a time, there was a King. Or there was a Prince. Oh, maybe, there wasn't a +prince, but probably there was a Programmer, hmm, okay, a programmer. And this +programmer knew Apache's mod_python. Yes, it was a very, very bad paragon, but +before Python, he was programming in PHP. So mod_python was a big movement in the +right direction at that time. + +He was finding out how he could write and host Python applications on a server. +And as he knew some closed-source framework that worked correctly, he wrote +another similar one for his own use. That is the basis of Poor Publisher. But WSGI was +coming, so he had an idea to write a new backend for his applications. That +is the basis of Poor HTTP and PoorWSGI. + +Sometimes, Poor HTTP and PoorWSGI were one project. It was a better way, but +that wasn't the right way. After some time, he divided these two projects into +PoorWSGI and Poor HTTP projects. But there was a flawed concept in the PoorWSGI +framework, which wasn't a framework in fact. So he looked for other projects and +saw how nice it could be to create a WSGI application for the user. That is when +PoorWSGI was rewritten into library-type code, and the application became a +callable class with some nice routing methods and decorators. + +This is the story of one programmer and his WSGI framework, which is not a +framework in fact, because it only handles URI requests with some mod_python +compatibility layer. As you can see, there are several ways this project can +evolve. Its author, the programmer, uses it on his projects, and it would be +very nice if there were more programmers than just him who used this little +project. Let's call it a WSGI connector. If you have any questions, proposals, bug fixes, text corrections, or any -other things, please send me email to *mcbig at zeropage.cz* or you can -create issue on GutHub: -https://github.com/PoorHttp/PoorWSGI/issues Thank you so much. +other matters, please send me an email to *mcbig at zeropage.cz*, or you can +create an issue on GitHub: https://github.com/PoorHttp/PoorWSGI/issues. +Thank you so much. ChangeLog ========= -For release history or difference of releases, you can use git diff, diff log, -git2cl tool or you can see ChangeLog from source code or on git repository -web. See: +For release history or differences between releases, you can use git diff, diff +log, the git2cl tool, or consult the ChangeLog from the source code or on the Git +repository's web page. See: https://github.com/PoorHttp/PoorWSGI/blob/master/doc/ChangeLog Examples ======== -It is published application test files. You can download it, study it, -test or use it as you can. See: +These are published application test files. You can download them, study them, +test them, or use them as you wish. See: **http_digest.py** https://github.com/PoorHttp/PoorWSGI/blob/master/examples/http_digest.py @@ -96,7 +96,7 @@ test or use it as you can. See: **put_file.py** - Example of uploading file via PUT method like in WebDAV + Example of uploading a file via the PUT method, similar to WebDAV. https://github.com/PoorHttp/PoorWSGI/blob/master/examples/put_file.py diff --git a/doc/documentation.rst b/doc/documentation.rst index ad43b63..7aae763 100644 --- a/doc/documentation.rst +++ b/doc/documentation.rst @@ -1,16 +1,16 @@ Responses --------- -The main goal of all WSGI middleware is return response corresponding to HTTP, -resp. WSGI request. Responding in PoorWSGI is just like other knows frameworks. +The main goal of all WSGI middleware is to return a response corresponding to an HTTP +or WSGI request. Responding in PoorWSGI is just like other known frameworks. Returning values ~~~~~~~~~~~~~~~~ Just value `````````` -The easiest way is return string or bytes. String values are automatically -convert to bytes, because it's WSGI internal. HTTP Response is 200 OK with -``text/html; character=utf-8"`` content type and default X-Powered-By header. +The easiest way is to return a string or bytes. String values are automatically +converted to bytes, because it is WSGI internal. The HTTP Response is 200 OK with +``text/html; charset=utf-8`` content type and a default X-Powered-By header. .. code:: python @@ -18,7 +18,7 @@ convert to bytes, because it's WSGI internal. HTTP Response is 200 OK with def some_path(req): return 'This is content for some path' -This examples returns the same values. +These examples return the same values. .. code:: python @@ -28,9 +28,9 @@ This examples returns the same values. Generator ````````` -Second way is return generator. You can return any iterable object, but it must -be always as first parameter, resp. that can't be tuple! -*See Returned parameters*. Generator must always return bytes! +The second way is to return a generator. You can return any iterable object, but it must +always be the first parameter; specifically, it cannot be a tuple! +*See Returned parameters*. A generator must always return bytes! .. code:: python @@ -39,7 +39,7 @@ be always as first parameter, resp. that can't be tuple! return [b'Hello ', b'world!'] -Or you can return any function which is generator. +Or you can return any function that is a generator. .. code:: python @@ -50,7 +50,7 @@ Or you can return any function which is generator. yield b'%d -> %x\n' % (i, i) return generator() -Or the handler could be generator. +Or the handler could be a generator. .. code:: python @@ -61,9 +61,9 @@ Or the handler could be generator. Returned parameters ``````````````````` -In fact, you can return more then one value. You can returned content type, -headers and status code next parameters. Python return all parameters as one -tuple. That is not need to append brackets around them. +In fact, you can return more than one value. You can return the content type, +headers, and status code as additional parameters. Python returns all parameters +as one tuple. There is no need to wrap them in brackets. .. code:: python @@ -71,7 +71,7 @@ tuple. That is not need to append brackets around them. def text_message(req): return "Hello world!", "text/plain" -The first argument can be still generator. +The first argument can still be a generator. .. code:: python @@ -82,7 +82,7 @@ The first argument can be still generator. yield b'%d -> %x\n' % (i, i) return generator(), "text/plain", () # empty headers -All values could looks like: +All values could look like this: .. code:: python @@ -96,8 +96,8 @@ Returning Responses make response ````````````` -Response are the base class fore returning values. In fact, from other values -which are returned from request handlers are converted to Response object, via +Response is the base class for returning values. In fact, other values which are +returned from request handlers are converted to a Response object via the make_response function. .. code:: python @@ -107,7 +107,7 @@ make_response function. data : str, bytes, dict, list, None or generator - Returned value as response body. Each type of data returns different + Returned value as response body. Each type of data returns a different response type: - str, bytes - Response @@ -116,15 +116,15 @@ data : str, bytes, dict, list, None or generator - generator - GeneratorResponse content_type : str - The ``Content-Type`` header which is set, if this header is not set - in headers. + The ``Content-Type`` header is set if this header is not already set + in the headers. headers : Headers, tuple, dict, ... - If is Headers instance, that be set *(referer)*. Other types, are send - to Headers constructor. + If it is a Headers instance, it will be set *(e.g., referer)*. Other types + are sent to the Headers constructor. status_code : int HTTP status code, HTTP_OK is 200. -You can use headers instead of `content_type` argument. +You can use the headers parameter instead of the `content_type` argument. .. code:: python @@ -134,8 +134,8 @@ You can use headers instead of `content_type` argument. headers={"Content-Type": "text/plain"}, status_code=NOT_FOUND) -If you return just simple type, or tuple of arguments, PoorWSGI automatically -call make_response function to create response for you. +If you return just a simple type, or a tuple of arguments, PoorWSGI automatically +calls the make_response function to create a response for you. .. code:: python @@ -153,14 +153,16 @@ call make_response function to create response for you. Response ```````` -Response object is one of base element of WSGI application. Response is object -which have full data, to return valid HTTP answer to client. Status code, -text reason of status code, headers and body. That's all. All values returned -from handlers is transform to Response object if it is possible. If handlers -return valid Response it will be returns. +A Response object is one of the basic elements of a WSGI application. Response is +an object that contains all the necessary data to return a valid HTTP answer to +the client: status code, text reason for the status code, headers, and body. +That's all. All values returned from handlers are transformed to a Response +object if possible. If a handler returns a valid Response, it will be returned +as-is. -Response have some functionality, to be useful like write method, to appending -to body with auto-counting ``Content-Length``, or some headers additional work. +Response has some useful functionality, such as the write method for appending +to the body with auto-counting of ``Content-Length``, and additional header +management. .. code:: python @@ -169,11 +171,11 @@ to body with auto-counting ``Content-Length``, or some headers additional work. return Response("I'm teapot :-)", content_type="text/plain", status_code=418) -There are some additional subclasses with special working. +There are some additional subclasses with specific functionality. JSONResponse ```````````` -There is JSONResponse class to fast way for returning JSON. +There is a JSONResponse class for quickly returning JSON. .. code:: python @@ -182,7 +184,7 @@ There is JSONResponse class to fast way for returning JSON. return JSONReponse(status_code=418, message="I'm teapot :-)", numbers=list(range(5))) -This response returned these data with status code 418: +This response returns the following data with status code 418: .. code:: json @@ -191,11 +193,11 @@ This response returned these data with status code 418: "numbers": [0, 1, 2, 3, 4] } -Or you can simple return dictionary or list. It will be automatically convert -to JSONResponse by make_response function. So it is similar to return text or -bytes. +Or you can simply return a dictionary or a list. It will be automatically +converted to JSONResponse by the make_response function. So it is similar to +returning text or bytes. -Be careful that your dict or list **have to be convertible** to JSON by +Be careful that your dict or list **has to be convertible** to JSON by the json.dumps function. .. code:: python @@ -214,10 +216,9 @@ json.dumps function. JSONGeneratorResponse ````````````````````` -There is JSONGeneratorResponse class too, which could return JSON, but -it could accept generators as arrays. And of course, this response -is returned by stream like GeneratorResponse, so data is not buffered -in memmory if wsgi server don't do that. +There is also a JSONGeneratorResponse class, which can return JSON and +can accept generators as arrays. This response is streamed like GeneratorResponse, +so data is not buffered in memory if the WSGI server does not buffer it. .. code:: python @@ -226,7 +227,7 @@ in memmory if wsgi server don't do that. return JSONGeneratorReponse(status_code=418, message="I'm teapot :-)", numbers=range(5)) -This response returned these data with status code 418: +This response returns the following data with status code 418: .. code:: json @@ -237,8 +238,9 @@ This response returned these data with status code 418: FileResponse ```````````` -File response open the file and send it throw ``wsgi.filewrapper``, which could -be *sendfile()* call. See PEP 3333. Content type and length read from system. +FileResponse opens the file and sends it through ``wsgi.filewrapper``, which +could be a *sendfile()* call. See PEP 3333. Content type and length are read +from the system. .. code:: python @@ -248,18 +250,18 @@ be *sendfile()* call. See PEP 3333. Content type and length read from system. GeneratorResponse ````````````````` -Response which is use for generator values. Generator **must** return bytes, -instead of strings! For string returned generator, use **StrGeneratorResponse**, -which use generator for utf-8 encoding to bytes. +A Response that is used for generator values. A generator **must** return bytes, +not strings. For a generator that returns strings, use **StrGeneratorResponse**, +which encodes the strings to UTF-8 bytes. NoContentResponse ````````````````` -Sometimes you don't want to response payload. NoContentResponse has default code -`204 No Content`. +Sometimes you don't want a response payload. NoContentResponse has a default +code of `204 No Content`. RedirectResponse ```````````````` -Response with interface for more comfortable redirect response. +A Response with an interface for a more comfortable redirect response. .. code:: python @@ -269,9 +271,9 @@ Response with interface for more comfortable redirect response. NotModifiedResponse ``````````````````` -NotModifiedResponse is base on NoContentResponse with status code -`304 Bot Modified`. You have to add some Not Modified header in headers -parameters or as constructor argument. +NotModifiedResponse is based on NoContentResponse with status code +`304 Not Modified`. You have to add a Not Modified header in the headers +parameters or as a constructor argument. .. code:: python @@ -298,9 +300,9 @@ parameters or as constructor argument. Partial Content ``````````````` -Sometimes, you want to return partial Content, which is typical reaction to -`Range` headers. For that situations, there are `parse_range` function and -`make_partial` Response method. +Sometimes, you want to return partial content, which is a typical reaction to +`Range` headers. For such situations, there are the `parse_range` function and +the `make_partial` Response method. .. code:: python @@ -323,7 +325,10 @@ Sometimes, you want to return partial Content, which is typical reaction to PartialResponse ``````````````` -For special use cases, programmer have own mechanism to select range, for example, if units is not bytes. For that situations, there is PartialResponse, which is similar to Response, but it is ``206 Partial Content`` yet, and you have to use ``make_range`` method to only create right ``Content-Range`` header. +For special use cases where a programmer has their own mechanism to select a range, +for example if units are not bytes, there is PartialResponse, which is similar +to Response, but is already set to ``206 Partial Content``, and you only need to +use the ``make_range`` method to create the correct ``Content-Range`` header. .. code:: python @@ -339,11 +344,11 @@ Stopping handlers HTTPException ````````````` -There is HTTPException class, based from Exception, which is used for stopping -handler with right http status. There is possible two scenarios. +There is the HTTPException class, based on Exception, which is used for stopping +a handler with the correct HTTP status. There are two possible scenarios: -You want to stop with specific HTTP status code, and handler from application -was used to generate right response. +You want to stop with a specific HTTP status code, and a handler from +the application will be used to generate the correct response. .. code:: python @@ -353,8 +358,8 @@ was used to generate right response. raise HTTPException(HTTP_BAD_REQUEST) return "Some message", "text/plain" -Or you would stop with specific response. Instead of status code, just use -Response object. +Or you want to stop with a specific response. Instead of a status code, just +use Response object. .. code:: python @@ -367,16 +372,16 @@ Response object. raise HTTPException(error) return "Other message", "text/plain" -**Additional functionality)** +**Additional functionality** -If status code is ``DECLINED``, that return nothing. That means, that no status +If the status code is ``DECLINED``, it returns nothing. That means no status code, no headers, no response body. Just stop the request. -If status code is ``HTTP_NO_CONTENT``, that return NoContentResponse, so message -body is not send. +If the status code is ``HTTP_NO_CONTENT``, it returns NoContentResponse, so the +message body is not sent. -When the handler raise any other exception, that generate Internal Server Error -status code. +When the handler raises any other exception, it generates an Internal Server +Error status code. Compatibility ````````````` @@ -385,34 +390,35 @@ functions. **redirect** -Have the same interface as RedirectResponse, and only raise the HTTPException +It has the same interface as RedirectResponse, and only raises the HTTPException with RedirectResponse. **abort** -Have the same interface as HTTPException, and voila, it raise the HTTPException. +It has the same interface as HTTPException, and voila, it raises the HTTPException. Routing ------- -There are two ways how to set path handler. Via decorators of Application object, -or method set\_ where one of parameter is your handler. It is important how look -your application. If your web project have one or a few files where your -handlers are, it is good idea to use decorators. But if you have big project -with more files, it could be difficult to load all files with decorated -handlers. So that is right job for set\_ methods in one file, like a route file -or dispatch table. +There are two ways to set a path handler: via decorators of the Application object, +or using a set\_ method where one of the parameters is your handler. The choice +depends on how your application is structured. If your web project has one or a few +files where your handlers are, it is a good idea to use decorators. But if you +have a large project with many files, it could be difficult to load all files with +decorated handlers. In that case, set\_ methods in a single file, such as a +route file or dispatch table, is a better approach. Static Routing ~~~~~~~~~~~~~~ -There are method and decorator to set your function (handler) to response static -route. Application.set_route and Application.route. Both of them have tho -parametrs, first the required path like ``/some/path/for/you`` and next method -flags, which is default METHOD_HEAD | METHOD_GET. There are other methods -in state module like METHOD_POST, METHOD_PUT etc. There is two special constants -METHOD_GET_POST which is HEAD | GET | POST, aned METHOD_ALL which is all -supported methods. If method does not match, but path is exist in internal -table, http state HTTP_METHOD_NOT_ALLOWED is return. +There is a method and a decorator to set your function (handler) to respond to a +static route: Application.set_route and Application.route. Both of them have +two parameters: first, the required path like ``/some/path/for/you``, and second, +method flags, which default to METHOD_HEAD | METHOD_GET. There are other +methods in the state module like METHOD_POST, METHOD_PUT, etc. There are two +special constants: METHOD_GET_POST, which is HEAD | GET | POST, and METHOD_ALL, +which includes all supported methods. If the method does not match but the path +exists in the internal table, the HTTP state HTTP_METHOD_NOT_ALLOWED is +returned. .. code:: python @@ -424,16 +430,17 @@ table, http state HTTP_METHOD_NOT_ALLOWED is return. return 'Data of other path' app.set_route('/some/other/path', other_path, state.METHOD_GET_POST) -You pop from application table via method Application.pop_route, or get internal -table via Application.routes property. **Each path can have only one handler**, -but one handler can be use for more path. +You can pop from the application table via method Application.pop_route, or get +the internal table via Application.routes property. **Each path can have only +one handler**, but one handler can be used for more paths. Regular expression routes ~~~~~~~~~~~~~~~~~~~~~~~~~ -As in other wsgi connectors, or frameworks if you want, there are way how to -define routes with getting part of url path as parameter of handler. PoorWSGI -call them **regular expression routes**. You can use it in nice human-readable -form or in your own regular expressions. Basic use is define by group name. +As in other WSGI connectors (or frameworks, if you prefer), there is a way to +define routes that capture part of the URL path as a parameter of the handler. +PoorWSGI calls them **regular expression routes**. You can use them in a nice +human-readable form or in your own regular expressions. Basic use is defined by +group name. .. code:: python @@ -442,10 +449,10 @@ form or in your own regular expressions. Basic use is define by group name. def user_detail(req, name): return 'Name is %s' % name -There are use filters define by regular expression from table -Application.filters. This filter is use to transport to regular expression -define by group. Default filter is ``r'[^/]+'`` with str convert function. You -can use any filter from table filters. +Filters are defined by regular expressions from the Application.filters table. +Each filter is used to transform a URL group into a regular expression. The default +filter is ``r'[^/]+'`` with a ``str`` convert function. You can use any filter +from the filters table. .. code:: python @@ -454,19 +461,20 @@ can use any filter from table filters. def surnames_by_age(req, surname, age): return 'Surname is: %s and age is: %d' % (surname, age) -Filter int is define by ``r'-?\d+'`` with convert "function" int. So age must be -number and the input parameter is int instance. +The :int filter is defined by ``r'-?\d+'`` with the ``int`` conversion function. +So age must be a number and the input parameter is an ``int`` instance. -There are predefined filters, for example: **:int**, **:word**, **:re:** and -**none** as default filter. Word is define as ``r'\w+'`` regular expression, -and poorwsgi use re.U flag, so it match any Unicode string. That means UTF-8 -string. For all filters see Application.filters property or ``/debug-info`` page. +There are predefined filters, for example: **:int**, **:word**, **:re:**, and +**none** as the default filter. :word is defined as the ``r'\w+'`` regular +expression, and PoorWSGI uses the ``re.U`` flag, so it matches any Unicode string +(i.e., UTF-8 string). For all filters, see the Application.filters property or +the ``/debug-info`` page. -You can get copy of filters table calling Application.filters property. And this -filters table is output to debug-info page. Adding your own filter is possible -with function set_filter with name, regular expression and convert function -which is str by default. Next you can use this filter in group regular -expression. +You can get a copy of the filters table by calling the Application.filters +property. This filters table is output to the debug-info page. Adding your own +filter is possible with the ``set_filter`` function, which takes a name, a +regular expression, and a convert function (which is ``str`` by default). You can +then use this filter in a group regular expression. .. code:: python @@ -476,9 +484,9 @@ expression. def user_by_login(req, login): return 'Users email is %s' % login -In other way, you can use filters define by inline regular expression. That is -``:re:`` filter. This filter have regular expression which you write in, and -allways str convert function, so parametr is allways string. +Alternatively, you can use filters defined by inline regular expressions. That is +the ``:re:`` filter. This filter takes a regular expression that you provide, and +always uses the ``str`` convert function, so the parameter is always a string. .. code:: python @@ -489,13 +497,13 @@ allways str convert function, so parametr is allways string. Group naming ~~~~~~~~~~~~ -Group names **must be unique** in defined path. They are store in ordered -dictionary, to do wrap by their convert functions. You can named them in route -definition how you can, and they can't be named same in handler parameters, -but they must be only in the same ordering. Be careful to named parameters -in handler with some python keyword, like class for example. If you can, you can -use python "varargs" syntax to get any count of parameters in your handler -function. +Group names **must be unique** in the defined path. They are stored in an +ordered dictionary and wrapped by their convert functions. You can name them +in the route definition as you wish; they do not need to match the parameter +names in the handler, but they must maintain the same ordering. Be careful not to +name parameters in the handler with a Python keyword, like ``class`` for example. If +you prefer, you can use Python's "varargs" syntax to receive any number of parameters +in your handler function. .. code:: python @@ -503,9 +511,9 @@ function. def test_varargs(req, *args): return "Parse %d parameters %s" % (len(args), str(args)) -At last future of regular expression routes is direct access to dictionary -with req.groups variable. This variable is set from any regular expression -route. +A future feature of regular expression routes is direct access to the dictionary +with the ``req.groups`` variable. This variable is set from any regular +expression route. .. code:: python @@ -513,24 +521,24 @@ route. def test_varargs(req, *args): return "All input variables from url path: %s" % str(req.groups) -Regular expression routes as like static routes could be set with -Application.route or Application.set_route methods. But internaly -Application.regular_route or Application.set_regular_route is call. -Same situation is with Application.pop_route and Application.pop_regular_route. +Regular expression routes, like static routes, can be set with Application.route +or Application.set_route methods. Internally, however, Application.regular_route +or Application.set_regular_route is called. The same situation applies to +Application.pop_route and Application.pop_regular_route. Other handlers -------------- Default handler ~~~~~~~~~~~~~~~ -If no route is match, there are two ways which could occur. First is call -default handler if method match of course. Default handler is set with default -Application.decorator or Application.set_default method. Parameter is only -method which is default in METHOD_HEAD | METHOD_GET too. Instead of route -handlers, when method does not match, 404 error was returned. +If no route matches, two scenarios can occur. The first is to call the default +handler if the method matches. The default handler is set with the default +Application decorator or Application.set_default method. The parameter is only +the method, which also defaults to METHOD_HEAD | METHOD_GET. Unlike route +handlers, when the method does not match, a 404 error is returned. -So default handler is fallback with ``r'/.*'`` regular expression. For example, -you can use is for any OPTIONS method. +So the default handler is a fallback with the ``r'/.*'`` regular expression. For +example, you can use it for any OPTIONS method. .. code:: python @@ -538,23 +546,23 @@ you can use is for any OPTIONS method. def default(req): return b'', '', {'Allow': 'OPTIONS', 'GET', 'HEAD'} -Be careful, default handler is call before 404 not found handler. When it is -possible to serve request any other way, it will. For example if -poor_DocumentRoot is set and PoorWSGI found the file, that will be send. -Of course, internal file or dictionary handler is use only with METHOD_GET -or METHOD_HEAD. +Be careful: the default handler is called before the 404 not found handler. If it +is possible to serve the request in any other way, it will be. For example, if +poor_DocumentRoot is set and PoorWSGI finds the file, it will be sent. Of +course, the internal file or directory handler is used only with METHOD_GET or +METHOD_HEAD. HTTP state handlers ~~~~~~~~~~~~~~~~~~~ -There are some predefined HTTP state handlers, which are use when other -HTTP state are raised via HTTPException or any other exception which ends with +There are some predefined HTTP state handlers, which are used when other HTTP +states are raised via HTTPException or any other exception that ends with an HTTP_INTERNAL_SERVER_ERROR status code. -You can redefined your own handlers for any combination of status code and -method type like routes handlers. Response from these handlers are same as in -route handlers. +You can define your own handlers for any combination of status code and method +type, similar to route handlers. Responses from these handlers are the same as +in route handlers. -Be sure, that some http_state handlers can add other keyword arguments. +Note that some HTTP state handlers receive additional keyword arguments. .. code:: python @@ -562,16 +570,17 @@ Be sure, that some http_state handlers can add other keyword arguments. def page_not_found(req, *_): return "Your request %s not found." % req.path, "text/plain" -If your http state (error) handler was crashed with error, internal server -error was return and right handler is called. If this your handler was crashed -too, default poor WSGI internal server error handler is called. +If your HTTP state (error) handler raises an error, a 500 Internal Server Error +is returned and the default internal server error handler is called. If your +default internal server error handler crashes as well, the built-in PoorWSGI +internal server error handler is called. Error handlers ~~~~~~~~~~~~~~ -In most cases, when exception was raised from your handler, *Internal Server -Error* was returned from server. When you want to handle each type of exception, -you can define your own error handler, which will be called instead of -HTTP_INTERNAL_SERVER_ERROR state handler. +In most cases, when an exception is raised from your handler, *Internal Server +Error* is returned from the server. When you want to handle each type of +exception, you can define your own error handler, which will be called instead +of the HTTP_INTERNAL_SERVER_ERROR state handler. .. code:: python @@ -592,34 +601,34 @@ HTTP_INTERNAL_SERVER_ERROR state handler. return "Yep!" -Exception handlers are stored in OrderedDict, so exception type is checked in -same order as you set error handlers. So you must define handler for base -exception last. +Exception handlers are stored in an OrderedDict, so the exception type is +checked in the same order as you set error handlers. Therefore, you must define +the handler for the base exception last. Before and After response ~~~~~~~~~~~~~~~~~~~~~~~~~ -PoorWSGI have too special list of handlers. First is iterate and call before -each response. You can add function with Application.before_response and +PoorWSGI also has two special lists of handlers. The first iterates and calls +before each response. You can add functions with Application.before_response and Application.after_response decorators or Application.add_before_response and -Application.add_after_response methods. And there are -Application.pop_before_response and Application.pop_after_response methods -to remove handlers. +Application.add_after_response methods. There are also +Application.pop_before_response and Application.pop_after_response methods to +remove handlers. -Before response handlers are called in order how was added to list. They don't -return anything, resp. their return values are ignored. If they crash with -error, internal_server_error was return and http state handler was called. +Before response handlers are called in the order they were added to the list. +Their return values are ignored. If they raise an error, an Internal Server Error +is returned and the HTTP state handler is called. -After response handlers are called in order how was added to list. If they -crash with error, internal_server_error was return and http state handler is -called, but all code from before response list and from route handler was -called. +After response handlers are called in the order they were added to the list. If +they raise an error, an Internal Server Error is returned and the HTTP state +handler is called, but all code from the before response list and from the +route handler has already been executed. -After response handler is call even if error handler, internal_server_error for -example was called. +An after response handler is called even if an error handler, such as +internal_server_error, was called. -Before response handler must have request argument, but after response handler -must have request and response argument. +A before response handler must have a request argument, but an after response +handler must have request and response arguments. .. code:: python @@ -635,29 +644,30 @@ must have request and response argument. Filtering ````````` -TODO: How to write output filter, gzip for example.... +TODO: How to write an output filter, gzip for example... WebSockets ~~~~~~~~~~ WebSockets are not directly supported in PoorWSGI, but upgrade requests can be -handled like other HTTP requests. See +handled like other HTTP requests. See the `websocket.py `_ -example which use uWsgi implementation or WSocket implementation. +example, which uses the uWSGI implementation or WSocket implementation. Request variables ----------------- -PoorWSGI has two extra classes for get arguments. From request path, typical -for GET method and from request body, typical for POST method. This parsing is -enabled by default, but you can configure with options. +PoorWSGI has two classes for parsing request arguments: one for arguments from +the request path (typical for GET requests) and one for arguments from the request +body (typical for POST requests). This parsing is enabled by default, but you can +configure it with options. Query arguments ~~~~~~~~~~~~~~~ -Request query arguments are stored to Args class, define in poorwsgi.request -module. Args is dict base class, with interface compatible methods getfirst -and getlist. You can access to variables with args parameters at all time when -poor_AutoArgs is set to On, which is default. +Request query arguments are stored in the Args class, defined in the +poorwsgi.request module. Args is a dict-based class with the getfirst and +getlist methods. You can access query variables via ``req.args`` whenever +poor_AutoArgs is set to On, which is the default. .. code:: python @@ -667,22 +677,23 @@ poor_AutoArgs is set to On, which is default. colors = req.args.getlist('color', func=int) return "Get arguments are %s" % str(req.args) -If no arguments are parsed, or if poor_AutoArgs is set to Off, req.args is -EmptyForm instance, which is dict base class too with both of methods. +If no arguments are parsed, or if poor_AutoArgs is set to Off, req.args is an +EmptyForm instance, which is also a dict-based class with both methods. Form arguments ~~~~~~~~~~~~~~ -Request form arguments are stored in FieldStorage class, define in -poorwsgi.fieldstorage module. This class is inspired by FieldStorage from -legacy cgi module. Variables are parsed every time, when poor_AutoForm is set -to On, which is default, request method is POST, PUT or PATCH and request -mime type is one of `Application.form_mime_types`. You can call it -on any other methods of course, but it must exist wsgi.input in request -environment from wsgi server. - -req.form instance is create with poor_KeepBlankValues and poor_StrictParsing -variables as Args class is create, but FieldStorageParser have file_callback -variable, which is configurable by Application.file_callback property. +Request form arguments are stored in the FieldStorage class, defined in the +poorwsgi.fieldstorage module. This class is inspired by FieldStorage from the +legacy cgi module. Variables are parsed whenever poor_AutoForm is set to +On (which is the default), the request method is POST, PUT or PATCH, and the +request MIME type is one of `Application.form_mime_types`. You can also trigger +this parsing for other methods, but ``wsgi.input`` must exist in the request +environment from the WSGI server. + +The ``req.form`` instance is created with poor_KeepBlankValues and +poor_StrictParsing variables, just as the Args class is created. However, +FieldStorageParser has a ``file_callback`` variable, which is configurable by the +Application.file_callback property. .. code:: python @@ -695,18 +706,18 @@ variable, which is configurable by Application.file_callback property. colors = req.form.getlist('color', func=int) return "Post arguments for id are %s" % (id, str(req.args)) -As like Args class, if poor_AutoForm is set to Off, or if method is no POST, -PUT or PATCH, req.form is EmptyForm instance instead of FieldStorage. +Similar to the Args class, if poor_AutoForm is set to Off, or if the method is +not POST, PUT or PATCH, ``req.form`` is an EmptyForm instance instead of +FieldStorage. JSON request ~~~~~~~~~~~~ -In the first place JSON request are from AJAX. There are automatic JSON -parsing in Request object, which parse request body to JSON variable. This -parsing starts only when Application.auto_json variable is set to True (default) -and if mime type of POST, PUT or PATCH request is application/json. -Then request body is parsed to json property. You can configure JSON types -via Application.json_mime_types property, which is list of request -mime types. +Initially, JSON requests came from AJAX. There is automatic JSON parsing in the +Request object, which parses the request body to a JSON variable. This parsing +starts only when the Application.auto_json variable is set to True (default) and +the MIME type of a POST, PUT or PATCH request is application/json. Then the +request body is parsed to the json property. You can configure JSON types via the +Application.json_mime_types property, which is a list of request MIME types. .. code:: python @@ -716,7 +727,7 @@ mime types. methods=state.METHOD_POST | state.METHOD_PUT | state.METHOD_PATCH) def test_json(req): for key, val in req.json.items(): - req.error_log('%s: %v' % (key, str(val))) + req.error_log('%s: %s' % (key, str(val))) res = Response(content_type='application/json') json.dump(res, {'Status': '200', 'Message': 'Ok'}) @@ -742,24 +753,24 @@ JQuery AJAX request could look like this: } }); -There are a few variants which req.json could be: +There are a few variants that req.json could be: -* JsonDict when dictionary is parsed. -* JsonList when list is parsed. -* Other based types from json.loads function like str, int, float, bool +* JsonDict when a dictionary is parsed. +* JsonList when a list is parsed. +* Other base types from the json.loads function, such as str, int, float, bool, or None. -* None when parsing of JSON fails. That is logged with WARNING log level. +* None when JSON parsing fails. This is logged with a WARNING log level. File uploading ~~~~~~~~~~~~~~ -By default, FieldStorage store files somewhere to ``/tmp`` directory. This is -happened in FieldStorageParser, which calls ``TemporaryFile``. Uploaded files -are accessible like another form variables, but. +By default, FieldStorage stores files somewhere in the ``/tmp`` directory. This +happens in FieldStorageParser, which calls ``TemporaryFile``. Uploaded files +are accessible like other form variables, but: -Any variables from FieldStorage is accessible with ``__getitem__`` method. -So you can get variable by ``req.form[key]``, which gets FieldStorage -instance. This instance has some attributes, which you can test, -what type of variable is it. +Any variable from FieldStorage is accessible with the ``__getitem__`` method. So +you can get a variable by ``req.form[key]``, which returns a FieldStorage +instance. This instance has some attributes that you can use to test what type +of variable it is. .. code:: python @@ -772,9 +783,9 @@ what type of variable is it. Own file callback ~~~~~~~~~~~~~~~~~ -Sometimes, you want to use your own file_callback, because you don't want to -use TemporaryFile as storage for this upload files. You can do it with simple -adding class, which is io.FileIO class in Python 3.x. Next only set +Sometimes, you want to use your own file_callback because you don't want to use +TemporaryFile as storage for uploaded files. You can do it by simply adding a +class that is an ``io.FileIO`` class in Python 3.x. Then, only set the Application.file_callback property. .. code:: python @@ -785,12 +796,11 @@ Application.file_callback property. app = Application('test') app.file_callback = FileIO -As you can see, this example works, but it is so bad solution of your problem. -Little bit better solution will be, if you store files only if exist and only -to special separate dictionary, which could be configurable. That you need use -to factory to create file_callback. In next example is written own form -processing, which is not important, when `file_callback` could be set via -Application property. +As you can see, this example works, but it is a poor solution to your problem. +A better solution is to store files only if they do not already exist, in a +configurable directory. You need to use a factory to create file_callback. The +following example shows custom form processing; however, this is not necessary +since ``file_callback`` can be set directly via an Application property. .. code:: python @@ -841,72 +851,72 @@ Application property. CachedInput ~~~~~~~~~~~ -When HTTP Forms are base64 encoded, FieldStorageParser use readline on request -input file. This is not so optimal. So there is CachedInput class, which -is returned as wrapper around ``wsgi.input`` file. +When HTTP forms are base64 encoded, FieldStorageParser uses readline on the +request input file. This is not optimal. CachedInput is a class that serves as +a wrapper around the ``wsgi.input`` file to address this. -Proccess variables +Process variables ~~~~~~~~~~~~~~~~~~ -Here is appliation variables, which is used to confiure request processing, -resp. which configure processing with request. +Here are the application variables used to configure request processing. Application.auto_args ````````````````````` -If auto_args is set to ``True``, which is default, Request object parse input -arguments from request uri at initialisation. There will be ``Request.args`` -property, which is instance of ``Args`` class. If you want to off this -functionality, set this property to ``False``. If argument parsing is disabled, -``Request.args`` will be instance of ``EmptyForm`` with same interface and no -data. +If ``auto_args`` is set to ``True`` (which is the default), the Request object +parses input arguments from the request URI at initialization. There will be a +``Request.args`` property, which is an instance of the ``Args`` class. If you want +to disable this functionality, set this property to ``False``. If argument +parsing is disabled, ``Request.args`` will be an instance of ``EmptyForm`` with +the same interface and no data. Application.auto_form ````````````````````` -If auto_form is set to ``True``, which is default, Request object parse input -arguments from request body at initialisation when request type is POST, PUT -or PATCH. There will be ``Request.form`` property which is instance of -``FieldStorage`` class. If you want to off this functionality, set this property -to ``False``. If form parsing is disabled, or JSON is detected, ``Request.form`` -will be instance of ``EmptyForm`` with same interface and no data. +If ``auto_form`` is set to ``True`` (which is the default), the Request object +parses input arguments from the request body at initialization when the request +type is POST, PUT or PATCH. There will be a ``Request.form`` property which is +an instance of the ``FieldStorage`` class. If you want to disable this +functionality, set this property to ``False``. If form parsing is disabled, or +JSON is detected, ``Request.form`` will be an instance of ``EmptyForm`` with the +same interface and no data. Application.form_mime_types `````````````````````````````` -List of mime types, which is parsed as input form by ``FieldStorageParser`` -class. If input request does not have set one of these mime types, that form -will not be parsed. +List of MIME types, which is parsed as an input form by the +``FieldStorageParser`` class. If the input request does not have one of these +MIME types set, that form will not be parsed. Application.file_callback ````````````````````````` -Class or function, which is used to store file from form. See +A class or function that is used to store a file from the form. See `own file callback`_ for more details. Application.auto_json ````````````````````` -If it is ``True``, which is default, method is POST, PUT or PATCH and request -mime type is json, than Request object do automatic parsing request body to -``Request.json`` dict property. If is disabled, or if form is detected, then -``EmptyForm`` instance is set. +If it is ``True`` (which is the default), the method is POST, PUT or PATCH and +the request mime type is JSON, then the Request object automatically parses +the request body to the ``Request.json`` dict property. If it is disabled, or if +a form is detected, then an ``EmptyForm`` instance is set. Application.json_mime_types `````````````````````````````` -List of mime types, which is paresed as json by ``json.loads`` function. -If input request does not have set one of these mime types, that -``Request.json`` was not parsed. +List of MIME types, which is parsed as JSON by the ``json.loads`` function. +If the input request does not have one of these MIME types set, then +``Request.json`` will not be parsed. Application.keep_blank_values ````````````````````````````` -This property is set for input parameters to automatically calling Args and -FieldStorageParser classes, when auto_args resp. auto_form is set. By default -this property is set to ``0``. If it set to ``1``, blank values should be -interpret as empty strings. +This property is passed to the Args and FieldStorageParser classes when +``auto_args`` and ``auto_form`` are set, respectively. +By default, this property is set to ``0``. If it is set to ``1``, blank values +will be interpreted as empty strings. Application.strict_parsing `````````````````````````` -This property is set for input parameter to automatically calling Args and -FieldStorageParser classes. When auto_args resp. auto_form is set. By default -this variable is set to ``0``. If is set to ``1``, ValueError exception -could raise on parsing error. I'm sure, that you never want to set this -variable to ``1``. If so, use it in your own parsing. +This property is passed to the Args and FieldStorageParser classes when +``auto_args`` and ``auto_form`` are set, respectively. +By default, this variable is set to ``0``. If it is set to ``1``, a ValueError +exception may be raised on a parsing error. You will almost certainly never want to +set this variable to ``1``; if you do, use it in your own parsing. .. code:: python @@ -936,16 +946,16 @@ variable to ``1``. If so, use it in your own parsing. Application.auto_cookies ```````````````````````` -When auto_cookies is set to ``True``, which is default, ``Request.cookies`` -property is set when request heades contains ``Cookie`` header. Otherwise -empty tupple will be set. +When ``auto_cookies`` is set to ``True`` (which is the default), the +``Request.cookies`` property is set when the request headers contain a ``Cookie`` +header. Otherwise, an empty tuple will be set. Application / User options -------------------------- -Like in mod_python Request, Poor WSGI Application have get_options method too. -This method return dictionary of application options or variables, which start -with ``app_`` prefix. This prefix is cut from options names. +Like mod_python's Request, the PoorWSGI Application has a get_options method. +This method returns a dictionary of application options, whose names start with +the ``app_`` prefix. This prefix is stripped from the option names. .. code:: ini @@ -965,7 +975,7 @@ And you can get these variables with get_options method: def list_options(req): return ("%s = %s" % (key, val) in config.items()) -Output of application url /options looks like: +The output of application URL /options looks like this: :: @@ -973,46 +983,46 @@ Output of application url /options looks like: tmp_path = tmp templ = templ -You can store your variables to request object too. There are few reserved -variables for you, which poorwsgi never use, and which are None by default: +You can also store your variables in the request object. There are a few reserved +variables for you, which PoorWSGI never uses, and which are ``None`` by default: :req.user: For user object, who is login, check_digest decorator set this variable. :req.api: For API checking. OpenAPIRequest use this variable. -:req.db: For single database conection per request. You can store structure - with more databases if you need to this vairable. -:req.app\_: As prefix for any your application variable. +:req.db: For a single database connection per request. You can store a + structure with multiple databases if needed. +:req.app\_: As a prefix for any of your application variables. -So if you want to add any other variable, be careful to named it. +So if you want to add any other variable, be careful how you name it. Headers and Sessions -------------------- Request Headers ~~~~~~~~~~~~~~~ -We talk about headers in a few paragraph before. Now is time to more -information about that. Request object have headers_in attribute, which is -instance of wshiref.headers.Headers. This headers contains request headers -from client like in mod_python. You can read it as you can. +Request headers were introduced earlier; this section provides more detail. +The Request object has a ``headers_in`` attribute, which +is an instance of ``wsgiref.headers.Headers``. These headers contain the request +headers from the client, similar to mod_python. You can read them as needed. -Next to it there are some Request properties, to get parset header values. +In addition, there are some Request properties for accessing parsed header values. :headers: Full headers object. :mime_type: Return mime type part from ``Content-Type`` header :charset: Return charset part from ``Content-Type`` header :content_length: Return content length if ``Content-Length`` header is set, or -1 if not. -:accept: List of ``Accept`` content neogetions set. -:accept_charset: List of ``Accept-Charset`` content neogetions set. -:accept_encoding: List of ``Accept-Encoding`` content neogetions set. -:accept_language: List of ``Accept-Language`` content neogetions set. +:accept: List of ``Accept`` content negotiations set. +:accept_charset: List of ``Accept-Charset`` content negotiations set. +:accept_encoding: List of ``Accept-Encoding`` content negotiations set. +:accept_language: List of ``Accept-Language`` content negotiations set. :accept_html: True if ``text/html`` mime type is in ``Accept`` header. :accept_xhtml: True if ``text/xhtml`` mime type is in ``Accept`` header. :accept_json: True if ``application/json`` mime type is in ``Accept`` header. :is_xhr: True if ``X-Requested-With`` is ``XMLHttpRequest``. -:cookies: Cooike object created from ``Cookie`` header or empty tuple. -:authorization: Parsed ``Authorization`` header to dictionary. -:referer: Http referer from ``Referer`` header or None +:cookies: Cookie object created from ``Cookie`` header or empty tuple. +:authorization: Parsed ``Authorization`` header as a dictionary. +:referer: HTTP referer from ``Referer`` header or None. :user_agent: User's client from ``User-Agent`` header or None. :forwarded_for: Value of ``X-Forward-For`` header or None. :forwarded_host: Value of ``X-Forward-Host`` header or None. @@ -1020,12 +1030,12 @@ Next to it there are some Request properties, to get parset header values. Response Headers ~~~~~~~~~~~~~~~~ -Response headers is the same Request.Headers class as in request object. But -you can create it. If you don't set header when you create Response object, -default ``X-Powered-By`` header is set to "Poor WSGI for Python". The -``Content-Type`` and ``Content-Length`` headers are append automatically. -All headers keys must be set once, except of ``Set-Cookie``, which could be set -more times. +Response headers use the same Headers class as in the request object. +If you don't set a header when you create a Response object, +the default ``X-Powered-By`` header is set to "Poor WSGI for Python". The +``Content-Type`` and ``Content-Length`` headers are appended automatically. Each +header key must appear at most once, except for ``Set-Cookie``, which can be set +multiple times. .. code:: python @@ -1040,10 +1050,10 @@ more times. Sessions ~~~~~~~~ -Like in mod_python, PoorSession is session class of PoorWSGI. It's -self-contained cookie which has data dictionary. Data are sent to client in -hidden, bzip2, base64 encoded format. PoorSession needs ``secret_key``, -which can be set by ``poor_SecretKey`` environment variable to +Like mod_python, PoorSession is the session class of PoorWSGI. It's a +self-contained cookie with a data dictionary. Data are sent to the client +in a hidden, bzip2-compressed, base64-encoded format. PoorSession needs a ``secret_key``, +which can be set by the ``poor_SecretKey`` environment variable to the Application.secret_key property. .. code:: python @@ -1105,20 +1115,21 @@ Application.secret_key property. HTTP Digest Auth ~~~~~~~~~~~~~~~~ -PoorWSGI supports HTTP Digest Authorization from version 2.3.x. -Supported are: +PoorWSGI supports HTTP Digest Authorization from version 2.3.x. Supported features +are: * MD5, MD5-sess, SHA-256, SHA-256-sess algorithm, **MD5-sess** is default * none or auth quality of protection (qop), **auth** is default * nonce value timeout, so new hash will be count every N seconds, **300** sec (5min) is default - * ``nc`` header value from browser **is not checked** on server side now + * The ``nc`` header value from the browser **is not currently checked** on the + server side. Application settings ```````````````````` -There are some application options, which are used for HTTP Authorization +There are some application options that are used for HTTP Authorization configuration. :secret_key: Secret Key is used for generating ``nonce`` value, @@ -1151,10 +1162,11 @@ configuration. Usage ````` -There is check_digest decorator, which can be used simply to check -``Authorization`` header in client requests. Be careful to overriding default -HTTP_UNAUTHORIZED handler, which must return right ``WWW-Authenticate`` header, -when browser doesn't sent right ``Authorization`` header. +There is a check_digest decorator, which can be used to check the +``Authorization`` header in client requests. Be careful when overriding the +default HTTP_UNAUTHORIZED handler - it must return the correct +``WWW-Authenticate`` header when the browser does not send a valid +``Authorization`` header. .. code:: python @@ -1171,8 +1183,8 @@ when browser doesn't sent right ``Authorization`` header. """Page only for *foo* user in *User Zone* only.""" ... -The poorwsgi.digest module can be use for managing digest file too. But you -can manage PasswordMap directly with methods. +The poorwsgi.digest module can also be used for managing the digest file. You +can also manage PasswordMap directly with its methods. .. code:: sh @@ -1185,35 +1197,34 @@ can manage PasswordMap directly with methods. Debugging --------- -Poor WSGI have few debugging mechanism which you can to use. First, it could -be good idea to set up poor_Debug variable. If this variable is set, there are -full traceback on error page internal_server_error with http code 500. - -Second effect of this variable is enabling special debug page on -``/debug-info`` url. On this page, you can found: - - * full handlers table with requests, http methods and handlers which are - call to serve this requests. - * http state handlers table with http state codes, http methods and handlers - which are call when this http state is returned. - * request headers table from your browser when you call this debug request - * poor request variables, which are setting of actual instance of Poor WSGI - configuration variables. - * application variables which are set like a connector variables but with - app\_ prefix. - * request environment, which is set from your wsgi server to wsgi - application, so to Poor WSGI connector. +Poor WSGI has a few debugging mechanisms you can use. First, it is a +good idea to set the poor_Debug variable. If this variable is set, there is a +full traceback on the internal_server_error page (HTTP 500). + +The second effect of this variable is enabling the special debug page at +``/debug-info`` URL. On this page, you can find: + + * a full handlers table with request paths, HTTP methods, and handlers that + are called to serve those requests. + * an HTTP state handlers table with HTTP status codes, HTTP methods, and + handlers that are called when those HTTP states are raised. + * the request headers sent by your browser when you access the debug page. + * the Poor WSGI configuration variables for the current application instance. + * application variables, which are set like connector variables but with + the app\_ prefix. + * the request environment, which is passed from the WSGI server to the WSGI + application, that is, to the Poor WSGI connector. Profiling ~~~~~~~~~ -If you want to profile your request code, you can do with profiler. Poor WSGI -application object have methods to set profiling. You must only prepare runctx -function, which is call before all your request. From each your request will -be generate .profile dump file, which you can study. +If you want to profile your request code, you can do so with a profiler. The Poor +WSGI application object has methods to set up profiling. You only need to provide +a runctx function, which is called before every request. For each request, a +.profile dump file will be generated for analysis. -If you want to profile all process after start your application, you can make -file, which profile importing your application, which import Poor WSGI -connector. +If you want to profile the entire process from the start of your application, +you can create a file that profiles the import of your application, which in +turn imports the Poor WSGI connector. .. code:: python @@ -1230,16 +1241,16 @@ connector. # web application app.set_profile(cProfile.runctx, 'log/req') -When you use this file instead of your application file, simple.py for -example, application create files in log directory. First file will be -init.profile from first import by WSGI server. Other files will look like -req\_.profile, req_debug-info.profile etc. Second parameter of set_profile -method is prefix of output file names. File name are create from url path, so -each url create file. +When you use this file instead of your application file (simple.py for +example), the application creates files in the log directory. The first file will +be init.profile, created from the initial import by the WSGI server. Other files +will look like req\_.profile, req_debug-info.profile, etc. The second parameter of +the set_profile method is the prefix for output file names. File names are created +from the URL path, so each URL creates its own file. -There is nice tool to view this profile files runsnakerun. You can download it -from http://www.vrplumber.com/programming/runsnakerun/. Using that is very -simple just open profile file: +There is a useful tool to view these profile files called runsnakerun. You can +download it from http://www.vrplumber.com/programming/runsnakerun/. Using it is +very simple - just open a profile file: .. code:: sh @@ -1249,14 +1260,14 @@ simple just open profile file: OpenAPI ------- -OpenAPI aka Swagger 3.0 is specification for RESTful api documentation and -request and response validation. PoorWSGI have -`openapi_core `_ wrapper in -``openapi_wrapper`` module. You must only declare your before and after request -handler. - -This wrapper is place where, **openapi_core** python package is use, so that is -not in PoorWSGI requirements. You need to install separately: +OpenAPI aka Swagger 3.0 is a specification for RESTful API documentation and +request and response validation. PoorWSGI has an +`openapi_core `_ wrapper in the +``openapi_wrapper`` module. You only need to declare your before and after +response handlers. + +This wrapper is the only place where the **openapi_core** Python package is +used, so it is not in PoorWSGI's requirements. You need to install it separately: .. code:: sh diff --git a/examples/http_digest.py b/examples/http_digest.py index 7bef4b3..39bc3cd 100644 --- a/examples/http_digest.py +++ b/examples/http_digest.py @@ -35,7 +35,7 @@ def get_header(title): - """Return HTML header list of lines.""" + """Returns an HTML header as a list of lines.""" return ( "", "", '', @@ -44,14 +44,14 @@ def get_header(title): def get_footer(): - """Return HTML footer list of lines.""" + """Returns an HTML footer as a list of lines.""" return ("
", "Copyright (c) 2020 Ondřej Tůma. See ", 'poorhttp.zeropage.cz' '.', "", "") def get_link(href, text=None, title=None): - """Return HTML anchor.""" + """Returns an HTML anchor.""" text = text or title or href title = title or text return f'{text}' @@ -59,7 +59,7 @@ def get_link(href, text=None, title=None): @app.route('/') def root(req): - """Return Root (Index) page.""" + """Returns the Root (Index) page.""" body = ('
    ', "
  • " + get_link('/admin_zone') + " - admin zone (admin/admin)
  • ", "
  • " + get_link('/user_zone') + " - user zone (user/looser;sha/sha)
  • ", @@ -79,7 +79,7 @@ def root(req): @app.route('/admin_zone') @check_digest(ADMIN) def admin_zone(req): - """Page only for ADMIN realm.""" + """Page accessible only by the ADMIN realm.""" body = (f'

    {ADMIN} test for {app.auth_algorithm} algorithm.

    ', '
      ', '
    • ' + get_link('/', 'Root') + '
    • ', '
    • ' + get_link('/admin_zone?arg=42', 'one more time') + '
    • ', @@ -92,7 +92,7 @@ def admin_zone(req): @app.route('/user_zone') @check_digest(USER) def user_zone(req): - """Page for USER realm.""" + """Page for the USER realm.""" body = (f'

      {USER} test for {app.auth_algorithm} algorithm.

      ', f'User: {req.user}', '
        ', '
      • ' + get_link('/', 'Root') + '
      • ', '
      • ' + @@ -106,7 +106,7 @@ def user_zone(req): @app.route('/user') @check_digest(USER, 'user') def user_only(req): - """Page for user only.""" + """Page accessible only by a specific user.""" body = (f'

        User test for {app.auth_algorithm} algorithm.

        ', f'User: {req.user}', '
          ', '
        • ' + get_link('/', 'Root') + '
        • ', '
        • ' + @@ -119,7 +119,7 @@ def user_only(req): @app.route('/foo') @check_digest(USER, 'foo') def foo_only(req): - """Page for foo user only.""" + """Page accessible only by the 'foo' user.""" body = (f'

          Foo test for {app.auth_algorithm} algorithm.

          ', f'User: {req.user}', '
            ', '
          • ' + get_link('/', 'Root') + '
          • ', '
          ', '
          ', @@ -134,7 +134,7 @@ def foo_only(req): @app.route('/user/utf-8') @check_digest(USER, 'Ondřej') def utf8_chars(req): - """Page for user only.""" + """Page accessible only by a specific user.""" body = (f'

          User test for {app.auth_algorithm} algorithm.

          ', f'User: {req.user}', '
            ', '
          • ' + get_link('/', 'Root') + '
          • ', '
          • ' + @@ -148,7 +148,7 @@ def utf8_chars(req): @app.route('/foo/passwd', method=state.METHOD_POST) @check_digest(USER, 'foo') def foo_password(req): - """Change foo's password.""" + """Changes the 'foo' user's password.""" digest = hexdigest(req.user, USER, req.form.get('password'), app.auth_hash) app.auth_map.set(USER, req.user, digest) redirect('/foo') @@ -157,12 +157,12 @@ def foo_password(req): @app.route('/unknown') @check_digest(USER, 'unknown') def unknown_endpoint(req): - """Page for digest test.""" + """Page for digest authentication testing.""" return EmptyResponse() def generic_response(url, user): - """Return generic response""" + """Returns a generic response.""" body = (f'

            {USER} test for {app.auth_algorithm} algorithm.

            ', f'User: {user}', '
              ', '
            • ' + get_link('/', 'Root') + '
            • ', '
            • ' + get_link(url + '?param=text', 'one more time') + '
            • ', @@ -175,21 +175,21 @@ def generic_response(url, user): @app.route('/spaces in url') @check_digest(USER) def spaces_in_url(req): - """URL with spaces in path.""" + """URL with spaces in the path.""" return generic_response(req.path, req.user) @app.route('/čeština v url') @check_digest(USER) def diacritics_in_url(req): - """URL with diacritics in path.""" + """URL with diacritics in the path.""" return generic_response(req.path, req.user) @app.route('/crazy in url 🤪') @check_digest(USER) def crazy_in_url(req): - """URL with unicode in path.""" + """URL with Unicode characters in the path.""" return generic_response(req.path, req.user) diff --git a/examples/large_file.py b/examples/large_file.py index c7fd5d1..f44ea38 100644 --- a/examples/large_file.py +++ b/examples/large_file.py @@ -29,7 +29,7 @@ class Blackhole: - """Dummy File Object""" + """A dummy file object.""" def __init__(self, filename): log.debug("Start uploading file: %s", filename) @@ -37,28 +37,27 @@ def __init__(self, filename): self.__hash = sha256() def write(self, data): - """Only count uploaded data size.""" + """Only counts the uploaded data size.""" size = len(data) self.uploaded += size self.__hash.update(data) return size def seek(self, size): - """Dummy seek""" + """A dummy seek method.""" if size == -1: return self.uploaded return size def hexdigest(self): - """Return sha256 hexdigest of file.""" - return self.__hash.hexdigest() + """Returns the SHA256 hexdigest of the file.""" def close(self): - """Dummy close""" + """A dummy close method.""" class Temporary: - """Temporary file""" + """A temporary file.""" def __init__(self, filename): log.debug("Start uploading file: %s", filename) @@ -68,37 +67,37 @@ def __init__(self, filename): self.__file = TemporaryFile('wb+') def write(self, data): - """Only count uploaded data size.""" + """Only counts the uploaded data size.""" size = self.__file.write(data) self.__hash.update(data) self.uploaded += size return size def seek(self, size): - """Proxy to internal file object seek method.""" + """Proxies to the internal file object's seek method.""" return self.__file.seek(size) def read(self, size): - """Proxy to internal file object read method.""" + """Proxies to the internal file object's read method.""" return self.__file.seek(size) def close(self): - """Proxy to internal file object close method.""" + """Proxies to the internal file object's close method.""" return self.__file.close() def hexdigest(self): - """Return sha256 hexdigest of file.""" + """Returns the SHA256 hexdigest of the file.""" return self.__hash.hexdigest() def blackhole_factory(req): - """Factory for craeting Dummy file instance""" + """Factory for creating a dummy file instance.""" if req.content_length <= 0: raise HTTPException(400, error="Missing content length or no content") def create(filename): - """Create Blackhole File object""" + """Creates a Blackhole File object.""" log.debug(create.__doc__) return Blackhole(filename) @@ -106,13 +105,13 @@ def create(filename): def temporary_factory(req): - """Factory for craeting Dummy file instance""" + """Factory for creating a dummy file instance.""" if req.content_length <= 0: raise HTTPException(400, error="Missing content length or no content") def create(filename): - """Create Temporary File object""" + """Creates a Temporary File object.""" log.debug(create.__doc__) return Temporary(filename) @@ -120,15 +119,15 @@ def create(filename): def no_factory(): - """No factory callback""" + """No factory callback function.""" def original_factory(): - """Original factory callback""" + """Original factory callback function.""" def html_form(req, file_callback): - """Generate upload page for specified callback.""" + """Generates an upload page for the specified callback.""" stats = "" hexdigest = "" if req.method == 'POST': @@ -198,25 +197,25 @@ def html_form(req, file_callback): @app.route('/blackhole', method=state.METHOD_GET_POST) def blackhole_form(req): - """Return form for blackhole callback.""" + """Returns the form for the blackhole callback.""" return html_form(req, blackhole_factory) @app.route('/temporary', method=state.METHOD_GET_POST) def temporary_form(req): - """Return form for temporary callback.""" + """Returns the form for the temporary callback.""" return html_form(req, temporary_factory) @app.route('/no-factory', method=state.METHOD_GET_POST) def no_form(req): - """Return form for no Formfield.""" + """Returns the form for no Formfield.""" return html_form(req, original_factory) @app.route('/') def root(req): - """Return Root (Index) page.""" + """Returns the Root (Index) page.""" assert req return """ @@ -245,10 +244,10 @@ def root(req): class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): """This class is identical to WSGIServer but uses threads to handle - requests by using the ThreadingMixIn. This is useful to handle weg - browsers pre-opening sockets, on which Server would wait indefinitely. + requests by using the ThreadingMixIn. This is useful to handle web + browsers pre-opening sockets, on which the server would wait + indefinitely. """ - multithread = True daemon_threads = True diff --git a/examples/metrics.py b/examples/metrics.py index d3fef1e..54f5ca2 100644 --- a/examples/metrics.py +++ b/examples/metrics.py @@ -17,7 +17,7 @@ class Metrics: - """Simple metrics class.""" + """A simple metrics class.""" requests = 0 response_time = 0 best_time = float('inf') @@ -25,14 +25,14 @@ class Metrics: @staticmethod def avg(): - """Return average response time.""" + """Returns the average response time.""" if Metrics.requests: return Metrics.response_time / Metrics.requests return 0 @staticmethod def measure(start_time): - """Do measure.""" + """Performs a measurement.""" Metrics.requests += 1 response_time = time() - start_time Metrics.response_time += response_time @@ -42,14 +42,14 @@ def measure(start_time): @app.after_response() def metrics_end(req, res): - """End measuring response time.""" + """Ends measuring response time.""" Metrics.measure(req.start_time) return res @app.route('/metrics') def metrics(req): - """Return response metrics:""" + """Returns response metrics.""" assert req return JSONResponse( requests=Metrics.requests, @@ -60,14 +60,14 @@ def metrics(req): @app.route('/') def root(req): - """Simple hello world response.""" + """A simple hello world response.""" assert req return "Hello World", "text/plain" @app.route('/json', method=state.METHOD_POST) def test_json(req): - """Simple POST method.""" + """A simple POST method.""" return JSONResponse(status_code=418, message="I'm teapot :-)", request=req.json) diff --git a/examples/openapi3.py b/examples/openapi3.py index 16fb10d..cf80517 100644 --- a/examples/openapi3.py +++ b/examples/openapi3.py @@ -1,8 +1,10 @@ -"""This is example and test application for PoorWSGI connector with openapi3 +"""This is an example and test application for the PoorWSGI connector with +OpenAPI 3 support. -This sample testing example is free to use, modify and study under same BSD -licence as PoorWSGI. So enjoy it ;) +This sample testing example is free to use, modify, and study under the same +BSD +license as PoorWSGI. Enjoy! """ from wsgiref.simple_server import make_server @@ -59,7 +61,7 @@ @app.before_response() def cors_request(req): - """CORS additional response for method OPTIONS.""" + """CORS additional response for the OPTIONS method.""" if req.uri.startswith("/p/"): return # endpoints for printers does not need CORS if req.method_number == state.METHOD_OPTIONS: @@ -71,7 +73,7 @@ def cors_request(req): @app.after_response() def cors_response(req, res): - """CORS additional headers in response.""" + """CORS additional headers in the response.""" if isinstance(req, Request): res.add_header("Access-Control-Allow-Origin", req.headers.get("Origin", "*")) @@ -81,7 +83,7 @@ def cors_response(req, res): @app.before_response() def before_each_response(req): - """Check API before process each response.""" + """Checks the API before processing each response.""" req.api = OpenAPIRequest(req) try: unmarshal_request(req.api, app.openapi_spec) @@ -99,7 +101,7 @@ def before_each_response(req): @app.after_response() def after_each_response(req, res): - """Check if ansewer is valid by OpenAPI.""" + """Checks if the answer is valid by OpenAPI.""" if not hasattr(req, "api"): req.api = OpenAPIRequest(req) try: @@ -117,14 +119,14 @@ def after_each_response(req, res): @app.route("/plain_text") def plain_text(req): - """Simple hello world example.""" + """A simple hello world example.""" assert req return "Hello world", "text/plain" @app.route("/response") def response_handler(req): - """Override content-type via header value.""" + """Overrides the Content-Type via a header value.""" assert req return Response( status_code=200, @@ -134,14 +136,14 @@ def response_handler(req): @app.route("/json/") def ajax_arg(req, arg): - """Ajax JSON example.""" + """An Ajax JSON example.""" assert req return json.dumps({"arg": arg}), "application/json" @app.route('/json', method=state.METHOD_POST | state.METHOD_PUT) def test_json(req): - """JSONResponse example""" + """JSONResponse example.""" assert req return JSONResponse(status_code=418, message="I'm teapot :-)", request=req.json) @@ -149,21 +151,21 @@ def test_json(req): @app.route("/arg/") def ajax_integer(req, arg): - """Simple JSON response with integer argument in path.""" + """A simple JSON response with an integer argument in the path.""" assert req return json.dumps({"arg": arg}), "application/json" @app.route("/arg/") def ajax_float(req, arg): - """Simple JSON response with float argument in path.""" + """A simple JSON response with a float argument in the path.""" assert req return json.dumps({"arg": arg}), "application/json" @app.route("/arg/") def ajax_uuid(req, arg): - """Simple JSON response with uuid argument in path.""" + """A simple JSON response with a UUID argument in the path.""" assert req return json.dumps({"arg": str(arg)}), "application/json" @@ -177,7 +179,7 @@ def method_raises_errror(req): @app.route('/login') def login(req): - """Set login cookie test.""" + """Sets a login cookie test.""" assert req cookie = PoorSession(app.secret_key) cookie.data['login'] = True @@ -188,7 +190,7 @@ def login(req): @app.route('/check/login') def check_login(req): - """Clear login cookie - logout test.""" + """Clears the login cookie - logout test.""" session = PoorSession(app.secret_key) session.load(req.cookies) if 'login' not in session.data: @@ -207,7 +209,7 @@ def check_api_key(req): @app.http_state(state.HTTP_NOT_FOUND) def not_found(req): - """404 NotFound test.""" + """404 Not Found test.""" return (json.dumps( {"error": f"Url {req.uri}, you are request not found"}), "application/json", None, 404) diff --git a/examples/put_file.py b/examples/put_file.py index 0e72ab1..ddbc98f 100644 --- a/examples/put_file.py +++ b/examples/put_file.py @@ -27,7 +27,7 @@ @app.route('/blackhole/', method=state.METHOD_PUT) def blackhole_put(req, filename: str): - """Upload file via PUT method like in webdav""" + """Uploads a file via the PUT method, similar to WebDAV.""" checksum = sha256() uploaded = 0 @@ -49,7 +49,7 @@ def blackhole_put(req, filename: str): @app.route('/temporary/', method=state.METHOD_PUT) def temporary_put(req, filename: str): - """Upload file via PUT method like in webdav""" + """Uploads a file via the PUT method, similar to WebDAV.""" checksum = sha256() uploaded = 0 @@ -72,7 +72,7 @@ def temporary_put(req, filename: str): @app.route('/') def root(req): - """Return Root (Index) page.""" + """Returns the Root (Index) page.""" assert req return """ @@ -134,10 +134,10 @@ def root(req): class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): """This class is identical to WSGIServer but uses threads to handle - requests by using the ThreadingMixIn. This is useful to handle weg - browsers pre-opening sockets, on which Server would wait indefinitely. + requests by using the ThreadingMixIn. This is useful to handle web + browsers pre-opening sockets, on which the server would wait + indefinitely. """ - multithread = True daemon_threads = True diff --git a/examples/simple.py b/examples/simple.py index 1c54e33..9c6db74 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,7 +1,8 @@ -"""This is example and test application for PoorWSGI connector. +"""This is an example and test application for the PoorWSGI connector. -This sample testing example is free to use, modify and study under same BSD -licence as PoorWSGI. So enjoy it ;) +This sample testing example is free to use, modify, and study under the same +BSD +license as PoorWSGI. Enjoy! """ import logging as log @@ -49,13 +50,16 @@ class MyValueError(ValueError): - """My value error""" + """A custom Value Error.""" class Storage(file): """File storage class created by StorageFactory.""" def __init__(self, directory, filename): + """Initializes the Storage class, creating a file at the specified + path. Raises OSError if the file already exists. + """ log.debug("directory: %s; filename: %s", directory, filename) self.path = directory + '/' + filename @@ -67,7 +71,7 @@ def __init__(self, directory, filename): class StorageFactory: - """Storage Factory do some code before creating file.""" + """Storage Factory performs some setup before creating a file.""" # pylint: disable=too-few-public-methods @@ -77,7 +81,7 @@ def __init__(self, directory): os.mkdir(directory) def create(self, filename): - """Create file in directory.""" + """Creates a file in the specified directory.""" if not filename: return BytesIO() return Storage(self.directory, filename) @@ -88,7 +92,7 @@ def create(self, filename): @app.before_response() def log_request(req): - """Log each request before processing.""" + """Logs each request before processing.""" log.info("Before response") log.info("Headers: %s", req.headers) log.info("Data: %s", req.data) @@ -96,8 +100,9 @@ def log_request(req): @app.before_response() def auto_form(req): - """ This is own implementation of req.form paring before any POST response - with own file_callback. + """This is a custom implementation of req.form parsing before any POST + response + with its own file_callback. """ if req.is_body_request or req.server_protocol == "HTTP/0.9": factory = StorageFactory('./upload') @@ -114,7 +119,7 @@ def auto_form(req): def get_crumbnav(req): - """Create crumb navigation from url.""" + """Creates crumb navigation from the URL.""" navs = [req.hostname] if req.uri == '/': navs.append('/') @@ -125,7 +130,7 @@ def get_crumbnav(req): def get_header(title): - """Return HTML header.""" + """Returns an HTML header.""" return ( "", "", '', @@ -135,14 +140,14 @@ def get_header(title): def get_footer(): - """Return HTML footer.""" + """Returns an HTML footer.""" return ("
              ", "Copyright (c) 2013-2021 Ondřej Tůma. See ", 'poorhttp.zeropage.cz' '.', "", "") def get_variables(req): - """Return some environment variables and it's values.""" + """Returns some environment variables and their values.""" usable = ("REQUEST_METHOD", "QUERY_STRING", "SERVER_NAME", "SERVER_PORT", "REMOTE_ADDR", "REMOTE_HOST", "PATH_INFO") return sorted( @@ -155,7 +160,7 @@ def get_variables(req): def check_login(fun): - """Check session cookie.""" + """Checks the session cookie.""" @wraps(fun) def handler(req): @@ -177,7 +182,7 @@ def handler(req): @app.route('/') def root(req): - """Return root index.""" + """Returns the root index page.""" buff = get_header("Index") + ( get_crumbnav(req), "
                ", @@ -220,7 +225,7 @@ def root(req): @app.route('/favicon.ico') def favicon(_): - """Return favicon.""" + """Returns the favicon.""" icon = b""" AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAD///8A////AP///wD///8AFRX/Bw8P/24ICP/IAgL/7wAA/+oAAP/GAAD/bQAA/wj///8A @@ -249,7 +254,7 @@ def favicon(_): @app.route('/style.css') def style(_): - """Return stylesheet.""" + """Returns the stylesheet.""" buff = """ body { width: 90%; max-width: 900px; margin: auto; padding-top: 30px; } @@ -271,7 +276,7 @@ def style(_): @app.route('/test/') @app.route('/test/static') def test_dynamic(req, variable=None): - """Test dynamics values.""" + """Tests dynamic values.""" if not variable and req.headers.get('ETag') == 'W/"0123"': return not_modified(req) @@ -311,7 +316,7 @@ def test_dynamic(req, variable=None): @app.route('/test/') @app.route('/test///') def test_varargs(req, *args): - """Handler for variable path agrs""" + """Handler for variable path arguments.""" var_info = {'len': len(args), 'uri_rule': html_escape(req.uri_rule)} for key, val in req.path_args.items(): var_info[key] = html_escape(repr(val)) @@ -343,7 +348,7 @@ def test_varargs(req, *args): @app.route('/login') def login(req): - """Create login session cookie.""" + """Creates a login session cookie.""" log.debug("Input cookies: %s", repr(req.cookies)) cookie = PoorSession(app.secret_key) cookie.data['login'] = True @@ -354,7 +359,7 @@ def login(req): @app.route('/logout') def logout(req): - """Destroy login session cookie.""" + """Destroys the login session cookie.""" log.debug("Input cookies: %s", repr(req.cookies)) cookie = PoorSession(app.secret_key) cookie.destroy() @@ -366,7 +371,7 @@ def logout(req): @app.route('/test/form', method=state.METHOD_GET_POST) @check_login def test_form(req): - """Form example""" + """A form example.""" # pylint: disable=consider-using-f-string # get_var_info = {'len': len(args)} var_info = OrderedDict(( @@ -445,7 +450,7 @@ def test_form(req): @app.route('/test/upload', method=state.METHOD_GET_POST) @check_login def test_upload(req): - """Upload file example.""" + """A file upload example.""" var_info = OrderedDict(( ('form_keys', req.form.keys()), ('form_value_names', ', '.join( @@ -511,7 +516,7 @@ def test_upload(req): @app.http_state(state.HTTP_NOT_FOUND) def not_found(req, *_): - """Not found example response.""" + """A not found example response.""" buff = ( "", "", @@ -533,14 +538,14 @@ def not_found(req, *_): @app.error_handler(ValueError) def value_error_handler(*_): - """ValueError exception handler example.""" + """A ValueError exception handler example.""" log.exception("ValueError") raise HTTPException(state.HTTP_BAD_REQUEST) @app.route('/test/empty') def test_empty(req): - """No content response""" + """No content response.""" assert req res = NoContentResponse() res.add_header("Super-Header", "SuperValue") @@ -595,14 +600,14 @@ def gen(): @app.route('/yield') def yielded(_): - """Simple response generator by yield.""" + """A simple response generator using yield.""" for i in range(10): yield b"line %d\n" % i @app.route('/chunked') def chunked(_): - """Generator response with Response class.""" + """A generator response with a Response class.""" def gen(): for i in range(10): @@ -613,7 +618,7 @@ def gen(): @app.route('/yield', state.METHOD_POST) def input_stream(req): - """Stream request handler""" + """Stream request handler.""" i = 0 # chunk must be read with extra method, uwsgi has own @@ -630,7 +635,7 @@ def input_stream(req): @app.route('/simple') def simple(req): - """Return simple.py with FileObjResponse""" + """Returns simple.py with FileObjResponse.""" assert req file_ = open(__file__, 'rb') # pylint: disable=consider-using-with return FileObjResponse(file_) @@ -638,7 +643,7 @@ def simple(req): @app.route('/simple.py') def simple_py(req): - """Return simple.py with FileResponse""" + """Returns simple.py with FileResponse.""" last_modified = int(getctime(__file__)) weak = urlsafe_b64encode( md5( # nosec @@ -665,44 +670,44 @@ def simple_py(req): @app.after_response() def log_response(_, res): - """Log after response created.""" + """Logs after the response is created.""" log.info("After response") return res @app.route('/internal-server-error') def method_raises_errror(_): - """Own internal server error test""" + """A custom internal server error test.""" raise RuntimeError('Test of internal server error') @app.route('/none') def none_no_content(_): - """Test for None response.""" + """Tests for a None response.""" @app.route('/bad-request') def bad_request(req): - """Endpoint raises ValueError exception.""" + """Endpoint raises a ValueError exception.""" assert req raise MyValueError("ValueError exception test.") @app.route('/forbidden') def forbidden(req): - """Test forbiden exception.""" + """Tests forbidden exception.""" raise HTTPException(state.HTTP_FORBIDDEN) @app.route('/not-modified') def not_modified_result(_): - """Test for raise not NotModifiedResponse""" + """Tests for raising a NotModifiedResponse.""" raise HTTPException(NotModifiedResponse(etag="012")) @app.route('/not-implemented') def not_implemented(req): - """Test not implemented exception""" + """Tests not implemented exception.""" raise HTTPException(state.HTTP_NOT_IMPLEMENTED) diff --git a/examples/simple_json.py b/examples/simple_json.py index f3f52b0..594fe6a 100644 --- a/examples/simple_json.py +++ b/examples/simple_json.py @@ -1,7 +1,7 @@ -"""This is example and test JSON application for PoorWSGI connector. +"""This is an example and test JSON application for the PoorWSGI connector. -This sample testing example is free to use, modify and study under same BSD -licence as PoorWSGI. So enjoy it ;) +This sample testing example is free to use, modify, and study under the same +BSD license as PoorWSGI. Enjoy! """ # pylint: disable=duplicate-code @@ -42,7 +42,7 @@ @app.route('/test/json', method=state.METHOD_GET_POST) def test_json(req): - """Test GET / POST json""" + """Tests GET / POST JSON requests.""" # numbers are complete list data = req.json if req.is_chunked_request: @@ -61,7 +61,7 @@ def test_json(req): @app.route('/test/json-generator', method=state.METHOD_GET) def test_json_generator(req): - """Test JSON Generator""" + """Tests the JSON Generator.""" # numbers are generator, which are iterate on output return JSONGeneratorResponse(status_code=418, message="I'm teapot :-)", numbers=range(5), @@ -70,32 +70,32 @@ def test_json_generator(req): @app.route('/profile') def get_profile(_): - """Returun PROFILE env variable""" + """Returns the PROFILE environment variable.""" return JSONResponse(PROFILE=PROFILE) @app.route('/timestamp') def get_timestamp(req): - """Return simple json with req.start_time timestamp""" + """Returns simple JSON with the req.start_time timestamp.""" return JSONResponse(timestamp=req.start_time) @app.route('/unicode') def get_unicode(_): - """Return simple JSON with contain raw unicode characters.""" + """Returns simple JSON containing raw Unicode characters.""" return JSONResponse(name="Ondřej Tůma", encoder_kwargs={"ensure_ascii": False}) @app.route('/dict') def get_dict(_): - """Return dictionary""" + """Returns a dictionary.""" return {"route": "/dict", "type": "dict"} @app.route('/list') def get_list(_): - """Return list""" + """Returns a list.""" return [["key", "value"], ["route", "/list"], ["type", "list"]] diff --git a/examples/websocket.py b/examples/websocket.py index 62299ef..122e99f 100644 --- a/examples/websocket.py +++ b/examples/websocket.py @@ -1,6 +1,6 @@ """WebSocket example. -to run with uWSGI (uWSGI needs SSL to working websockets): +To run with uWSGI (uWSGI needs SSL for working websockets): .. code:: sh @@ -9,13 +9,13 @@ --gevent 100 \ --wsgi-file examples/websocket.py -without uWsgi WSocket package is used: +Without uWSGI, the WSocket package is used: .. code:: sh pip install WSocket -to proxy with nginx: +To proxy with nginx: .. code:: nginx @@ -64,22 +64,22 @@ WebSocketError = OSError def WSocketApp(var): # noqa: N802 - """Compatible with wsocket WSocketApp""" + """Compatible with wsocket.WSocketApp.""" return var class WebSocket(): - """Compatibility class.""" + """A compatibility class.""" # pylint: disable=no-self-use def __init__(self): uwsgi.websocket_handshake() def receive(self): - """Receive message from websocket.""" + """Receives a message from the websocket.""" return uwsgi.websocket_recv() def send(self, msg): - """Send message to websocket.""" + """Sends a message to the websocket.""" uwsgi.websocket_send(msg) @@ -92,12 +92,12 @@ def send(self, msg): def get_websocket(environment): - """Return websocket instace.""" + """Returns a websocket instance.""" if uwsgi: return WebSocket() def receive(self): - """uWsgi returns bytes.""" + """uWSGI returns bytes.""" string = self.receive_str() if string is None: raise WebSocketError("Socket was closed.") @@ -121,7 +121,7 @@ def receive(self): @poor.route('/') def root(req): - """Return Root (Index) page.""" + """Returns the Root (Index) page.""" ws_scheme = 'wss' if req.scheme == 'https' else 'ws' return """ @@ -196,7 +196,7 @@ def root(req): @poor.route('/ws') def websocket(req): - """Websocket endpoint""" + """WebSocket endpoint.""" answers = ("Hmm", "Yee", "Ok", "Really?", "Never mind", "You are best!", "😀", "😉", "☺", "😎", "👌", "👍", "🤔", "👏", "🤩", "...") try: @@ -218,10 +218,10 @@ def websocket(req): class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): """This class is identical to WSGIServer but uses threads to handle - requests by using the ThreadingMixIn. This is useful to handle weg - browsers pre-opening sockets, on which Server would wait indefinitely. + requests by using the ThreadingMixIn. This is useful to handle web + browsers pre-opening sockets, on which the server would wait + indefinitely. """ - multithread = True daemon_threads = True diff --git a/poorwsgi/__init__.py b/poorwsgi/__init__.py index 8564a51..105b456 100644 --- a/poorwsgi/__init__.py +++ b/poorwsgi/__init__.py @@ -4,23 +4,21 @@ Current Contents: * headers: Headers -* request: Request and FieldStorage classes, which is used for +* request: Request and FieldStorage classes, which are used for managing requests. -* response: Response classes and some make responses functions for creating - request response. -* results: default result handlers of connector like directory index, - servers errors or debug output handler. -* session: self-contained cookie based session class -* state: constants like http status code and method types -* wsgi: Application callable class, which is the main point for poorwsgi web - application. +* response: Response classes and functions for creating HTTP responses. +* results: Default result handlers for the connector, such as directory index, + server errors, or debug output handlers. +* session: A self-contained cookie-based session class. +* state: Constants like HTTP status codes and method types. +* wsgi: The Application callable class, which is the main entry point for a + PoorWSGI web application. * digest: HTTP Digest Authorization support. * openapi_wrapper: OpenAPI core wrapper for PoorWSGI Request and Response - object + objects. """ -from poorwsgi.response import redirect, abort, make_response - +from poorwsgi.response import abort, make_response, redirect from poorwsgi.wsgi import Application -__all__ = ["Application", "redirect", "abort", "make_response"] +__all__ = ["Application", "abort", "make_response", "redirect"] diff --git a/poorwsgi/digest.py b/poorwsgi/digest.py index 60a2417..739f358 100644 --- a/poorwsgi/digest.py +++ b/poorwsgi/digest.py @@ -1,6 +1,6 @@ """HTTP Authenticate Digest method. -This file could be used as known ``htdigest`` tool. +This file can be used as a standalone ``htdigest``-like tool. .. code:: sh @@ -31,11 +31,10 @@ def check_response(req, password): - """Check digest response value. + """Checks the digest response value. - Return True if response value is right. + Returns True if the response value is correct. """ - kwargs = req.authorization.copy() kwargs['hash1'] = password @@ -73,10 +72,10 @@ def check_response(req, password): def check_credentials(req, realm, username=None): - """Check Digest authorization credentials. + """Checks Digest authorization credentials. - Return True if Authorization header is valid for realm. - Username is checked too, if it is set. + Returns True if the Authorization header is valid for the realm. + The username is also checked if it is set. """ # pylint: disable=too-many-return-statements @@ -129,13 +128,13 @@ def check_credentials(req, realm, username=None): def check_digest(realm, username=None): - """Check HTTP Digest Authenticate. + """Checks HTTP Digest Authentication. - Allow only valid HTTP Digest authorization for realm. Username - is checked too, if it is set. When no, HTTP_UNAUTHORIZED response was - raised with realm and stale value if is need. + Allows only valid HTTP Digest authorization for the realm. The username + is also checked if it is set. If not, an HTTP_UNAUTHORIZED response is + raised with the realm and stale value, if needed. - When user is valid, req.user attribute is set to username. + If the user is valid, the req.user attribute is set to the username. .. code :: python @@ -180,9 +179,9 @@ def handler(req): def hexdigest(username, realm, password, algorithm=md5): - """Return digest hash value for user password. + """Returns the digest hash value for a user's password. - Return algorithm(username:realm:password).hexdigest() + Returns algorithm(username:realm:password).hexdigest(). """ return algorithm( ('%s:%s:%s' % (username, realm, password)).encode() @@ -190,38 +189,38 @@ def hexdigest(username, realm, password, algorithm=md5): class PasswordMap(defaultdict): - """Simple memory object to store user password. + """A simple memory object to store user passwords. Attributes: pathname : str - Full path to password file, must be set for PasswordMap.write - and PasswordMap.load methods. + The full path to the password file; must be set for + PasswordMap.write and PasswordMap.load methods. """ def __init__(self, pathname=None): super().__init__(dict) self.pathname = pathname def set(self, realm, username, digest): - """Add username to realm.""" + """Adds a username to the realm.""" self[realm][username] = digest def delete(self, realm, username): - """Delete username from realm.""" + """Deletes a username from the realm.""" return bool(self[realm].pop(username, None)) def find(self, realm, username): - """Return digest for username in realm if exist.""" + """Returns the digest for a username in the realm if it exists.""" if realm in self and username in self[realm]: return self[realm][username] return None def verify(self, realm, username, digest): - """Check digest in password map.""" + """Checks the digest in the password map.""" digest_ = self.find(realm, username) return bool(digest_) and digest_ == digest def load(self): - """Load map from file.""" + """Loads the map from a file.""" if self.pathname is None: raise RuntimeError("No pathname was set.") @@ -231,7 +230,7 @@ def load(self): self.set(realm, username, digest) def write(self): - """Write memory map dump.""" + """Writes the memory map dump.""" if self.pathname is None: raise RuntimeError("No pathname was set.") @@ -242,7 +241,7 @@ def write(self): def get_re_type(): - """Get password from stdin with re-type .""" + """Gets a password from stdin with re-type confirmation.""" password = getpass('New password: ') re_type = getpass('Re-type new password: ') if password != re_type: @@ -252,7 +251,7 @@ def get_re_type(): def main(): # noqa: C901 - """Main function for manipulation with passwordfile.""" + """Main function for manipulating the password file.""" # pylint: disable=too-many-return-statements # pylint: disable=too-many-statements # pylint: disable=too-many-branches diff --git a/poorwsgi/fieldstorage.py b/poorwsgi/fieldstorage.py index c828836..e38bb04 100644 --- a/poorwsgi/fieldstorage.py +++ b/poorwsgi/fieldstorage.py @@ -1,4 +1,4 @@ -"""PoorWSGI reimplementation of legacy cgi.FieldStorage. +"""PoorWSGI reimplementation of the legacy cgi.FieldStorage. :Classes: FieldStorage, FieldStorageParser :Functions: valid_boundary @@ -20,7 +20,7 @@ def valid_boundary(data): - """Check valid boundary label. + """Checks for a valid boundary label. >>> valid_boundary("----WebKitFormBoundaryMPRpF8CUUmlmqKqy") True @@ -35,9 +35,8 @@ def valid_boundary(data): class FieldStorageInterface(metaclass=ABCMeta): """FieldStorage Interface - Implements methods getvalue, getfirst and getlist + Implements the methods getvalue, getfirst, and getlist. """ - @abstractmethod def __contains__(self, key: str) -> bool: ... @@ -46,16 +45,15 @@ def __getitem__(self, key: str): ... def getvalue(self, key: str, default: Any = None, func: Callable = lambda x: x): - """Get but func is called for all values. - - Arguments: - key : str - key name - default : None - default value if key not found - func : converter (lambda x: x) - Function or class which processed value. Default type of value - is bytes for files and string for others. + """Returns the value for the given key, applying func to it. + + key + The key name. + default + The default value if the key is not found. + func + The function or class that processes the value. The default + type of value is bytes for files and string for others. """ if key in self: return func(self[key]) @@ -64,14 +62,15 @@ def getvalue(self, key: str, default: Any = None, def getfirst(self, key: str, default: Any = None, func: Callable = lambda x: x, fce: Optional[Callable] = None): - """Get first item from list for key or default. - - default : any - Default value if key not exists. - func : converter - Function which processed value. - fce : deprecated converter name - Use func converter just like getvalue. + """Gets the first item from a list for a key, or a default value. + + default + The default value if the key does not exist. + func + The function that processes the value. + fce + Deprecated converter name. Use the func converter, just like + getvalue. """ if fce: warnings.warn("Using deprecated fce argument. Use func instead.", @@ -87,14 +86,15 @@ def getfirst(self, key: str, default: Any = None, def getlist(self, key: str, default: Optional[list] = None, func: Callable = lambda x: x, fce: Optional[Callable] = None): - """Returns list of variable values for key or empty list. - - default : list or None - Default list if key not exists. - func : converter - Function which processed each value. - fce : deprecated converter name - Use func converter just like getvalue. + """Returns a list of variable values for a key, or an empty list. + + default + The default list if the key does not exist. + func + The function that processes each value. + fce + Deprecated converter name. Use the func converter, just like + getvalue. """ if fce: warnings.warn("Using deprecated fce argument. Use func instead.", @@ -109,41 +109,45 @@ def getlist(self, key: str, default: Optional[list] = None, class FieldStorage(FieldStorageInterface): - """Class inspired by cgi.FieldStorage. + """A class inspired by cgi.FieldStorage. - Instead of FieldStorage from cgi module, this is only storage for fields, - with some additional functionality in getfirst, getlist, getvalue or simple - get method. They return values instead of __getitem__ ([]), which returns - another FieldStorage. + Instead of FieldStorage from the cgi module, this is only storage for + fields, with some additional functionality in getfirst, getlist, + getvalue, or simple get methods. They return values instead of + __getitem__ ([]), which returns another FieldStorage. Available attributes: - :name: variable name, the same name from input attribute. - :value: property which returns content of field - :type: mime-type of variable. All variables have internal - mime-type, if that is no file, mime-type is text/plain. - :type_options: other content-type parameters, just like encoding. - :disposition: content disposition header if is set - :disposition_options: other content-disposition parameters if are set. - :filename: if variable is file, filename is its name from form. - :length: field length if was set in header, -1 by default. - :file: file type instance, from you can read variable. This - instance could be TemporaryFile as default for files, - StringIO for normal variables or instance of your own file - type class, create from file_callback. - :lists: if variable is list of variables, this contains instances - of other fields. - - FieldStorage is create by FieldStorageParser. - - FieldStorage has context methods, so you cat read files like this: + :name: The variable name, the same name as from the input + attribute. + :value: A property that returns the content of the field. + :type: The MIME type of the variable. All variables have an + internal MIME type; if it is not a file, the MIME + type is text/plain. + :type_options: Other Content-Type parameters, such as encoding. + :disposition: The Content-Disposition header, if set. + :disposition_options: Other Content-Disposition parameters, if set. + :filename: If the variable is a file, filename is its name + from the form. + :length: The field length if it was set in the header; + -1 by default. + :file: A file type instance from which you can read the + variable. This instance can be a TemporaryFile + (default for files), StringIO (for normal variables), + or an instance of your own file type class, created + from file_callback. + :lists: If the variable is a list of variables, this contains + instances of other fields. + + FieldStorage is created by FieldStorageParser. + + FieldStorage has context methods, so you can read files like this: >>> field = FieldStorage("key") >>> field.file = StringIO("value") >>> with field: ... print(field.value) value """ - name: Optional[str] = None filename: Optional[str] = None length: int @@ -228,7 +232,7 @@ def __contains__(self, key: str): return any(item.name == key for item in self.list) def __getitem__(self, key: str): - """Returns field if exist. + """Returns the field if it exists. >>> field = FieldStorage() >>> field.list = [FieldStorage("key", "value")] >>> field["key"].value @@ -249,12 +253,12 @@ def __getitem__(self, key: str): @property def value(self) -> Optional[Union[str, bytes, list]]: - """Return content of field. + """Returns the content of the field. - * If field is file, return it's content. - * If field is string value, return string. - * If field is list of other fields (root FieldStorage), return that - list. + * If the field is a file, its content is returned. + * If the field is a string value, the string is returned. + * If the field is a list of other fields (root FieldStorage), that + list is returned. >>> field = FieldStorage() >>> print(field.value) @@ -289,7 +293,7 @@ def value(self) -> Optional[Union[str, bytes, list]]: return value def keys(self): - """Dictionary like keys() method. + """A dictionary-like keys() method. >>> field = FieldStorage() >>> field.list = [FieldStorage("key", "value")] @@ -299,9 +303,10 @@ def keys(self): return dict.fromkeys(k.name for k in self.list).keys() def get(self, key: str, default: Any = None): - """Compatibility methods with dict. + """Compatibility method with dict. - Return value of list of values if exists. + Returns the field value, or a list of values if multiple exist + for the key. >>> field = FieldStorage() >>> field.list = [FieldStorage("key", "value")] @@ -319,16 +324,15 @@ def get(self, key: str, default: Any = None): def getvalue(self, key: str, default: Any = None, func: Callable = lambda x: x): - """Get but func is called for all values. + """Returns the value for the given key, applying func to each value. - Arguments: - key : str - key name - default : None - default value if key not found - func : converter (lambda x: x) - Function or class which processed value. Default type of value - is bytes for files and string for others. + key + The key name. + default + The default value if the key is not found. + func + The function or class that processes the value. The default + type of value is bytes for files and string for others. >>> field = FieldStorage() >>> field.list = [FieldStorage("key", "42")] @@ -345,10 +349,12 @@ def getvalue(self, key: str, default: Any = None, def getfirst(self, key: str, default: Any = None, func: Callable = lambda x: x, fce: Optional[Callable] = None): - """Get first item from list for key or default. + """Gets the first item from a list for a key, or a default value. - Use func converter just like getvalue. - :fce: deprecated converter name. + Uses the func converter just like getvalue. + + fce + Deprecated converter name. >>> field = FieldStorage() >>> field.list = [FieldStorage("key", "1"), FieldStorage("key", "2")] @@ -369,10 +375,12 @@ def getfirst(self, key: str, default: Any = None, def getlist(self, key: str, default: Optional[list] = None, func: Callable = lambda x: x, fce: Optional[Callable] = None): - """Returns list of variable values for key or empty list. + """Returns a list of variable values for a key, or an empty list. + + Uses the func converter just like getvalue. - Use func converter just like getvalue. - :fce: deprecated converter name + fce + Deprecated converter name. >>> field = FieldStorage() >>> field.list = [FieldStorage("key", "1"), FieldStorage("key", "2")] @@ -396,16 +404,17 @@ def getlist(self, key: str, default: Optional[list] = None, class FieldStorageParser: - """Class inspired by cgi.FieldStorage. + """A class inspired by cgi.FieldStorage. - This is only parsing part of old FieldStorage. It contain methods for - parsing POST form encoede in multipart/form-data or - application/x-www-form-urlencoded which is default. + This is only the parsing part of the old FieldStorage. It contains + methods for parsing POST forms encoded in multipart/form-data or + application/x-www-form-urlencoded, which is the default. - It generate FieldStorage or Field in depennd on encoding. But it do it - only from request body. FieldStorage has internal StringIO for all - values which are not stored in file. Some small binary files can be stored - in BytesIO. Limit for storing fields in temporary files is 8192 bytes. + It generates FieldStorage or Field depending on the encoding, but + it does so only from the request body. FieldStorage has an internal + StringIO for all values that are not stored in a file. Some small + binary files can be stored in BytesIO. The limit for storing fields + in temporary files is 8192 bytes. .. code:: python @@ -420,44 +429,53 @@ def __init__(self, input_=None, headers=None, outerboundary=b'', keep_blank_values=0, strict_parsing=0, limit=None, encoding='utf-8', errors='replace', max_num_fields=None, separator='&', file_callback=None): - """Constructor. Read multipart/* until last part. + """Constructor. Reads multipart/* until the last part. Arguments, all optional: - :input\\_: Request.input file object + input\\_ + Request.input file object. - :headers: header dictionary-like object + headers + A header dictionary-like object. - :outerboundary: terminating multipart boundary - (for internal use only) + outerboundary + The terminating multipart boundary (for internal use only). - :keep_blank_values: flag indicating whether blank values in + keep_blank_values + A flag indicating whether blank values in percent-encoded forms should be treated as blank strings. A true value indicates that blanks should be retained as - blank strings. The default false value indicates that + blank strings. The default false value indicates that blank values are to be ignored and treated as if they were not included. - :strict_parsing: flag indicating what to do with parsing errors. - If false (the default), errors are silently ignored. - If true, errors raise a ValueError exception. - - :limit: used internally to read parts of multipart/form-data forms, - to exit from the reading loop when reached. It is the difference - between the form content-length and the number of bytes already - read - - :encoding, errors: the encoding and error handler used to decode the - binary stream to strings. Must be the same as the charset defined - for the page sending the form (content-type : meta http-equiv or - header) - - :max_num_fields: int. If set, then parse throws a ValueError + strict_parsing + A flag indicating what to do with parsing + errors. If False (the default), errors are silently ignored. + If True, errors raise a ValueError exception. + + limit + Used internally to read parts of + multipart/form-data forms, to exit from the reading loop + when reached. It is the difference between the form's + content-length and the number of bytes already read. + + encoding, errors + The encoding and error handler used to + decode the binary stream to strings. Must be the same as the + charset defined for the page sending the form (content-type : + meta http-equiv or header). + + max_num_fields + If set, then parse throws a ValueError if there are more than n fields read by parse_qsl(). - :file_callback: function returns file class for own handling creating - files for write operations. By this, you can write file from - request direct to destionation without temporary files. + file_callback + A function that returns a file class for + custom handling of creating files for write operations. This + allows you to write a file from the request directly to its + destination without temporary files. """ self.headers = headers self.outerboundary = outerboundary @@ -483,18 +501,18 @@ def __init__(self, input_=None, headers=None, outerboundary=b'', self.input = input_ def _parse_content_type(self): - """ Process content-type header + """Processes the Content-Type header. - Honor any existing content-type header. But if there is no - content-type header, use some sensible defaults. Assume + Honors any existing Content-Type header. If no + Content-Type header is present, sensible defaults are used. Assumes outerboundary is "" at the outer level, but something non-false - inside a multi-part. The default for an inner part is text/plain, - but for an outer part it should be urlencoded. This should catch - bogus clients which erroneously forget to include a content-type + inside a multi-part. The default for an inner part is text/plain, + but for an outer part it should be urlencoded. This should catch + bogus clients that erroneously forget to include a Content-Type header. - See below for what we do if there does exist a content-type header, - but it happens to be something we don't understand. + See below for what is done if a Content-Type header exists, + but it is not understood. """ if 'content-type' in self.headers: ctype, pdict = parse_header(self.headers['content-type']) @@ -505,7 +523,7 @@ def _parse_content_type(self): return ctype, pdict def parse(self) -> FieldStorage: - """Read input and generate FieldStorage from that.""" + """Reads input and generates FieldStorage from it.""" field = FieldStorage() @@ -547,7 +565,7 @@ def parse(self) -> FieldStorage: return field def read_urlencoded(self): - """Internal: read data in query string format.""" + """Internal: Reads data in query string format.""" qs = self.input.read(self.length) if not isinstance(qs, bytes): msg = f"{self.input} should return bytes, got {type(qs).__name__}" @@ -562,7 +580,7 @@ def read_urlencoded(self): return [FieldStorage(key, value) for key, value in query] def _skip_to_boundary(self): - """Check and read file until we've hit our inner boundary.""" + """Checks and reads the file until the inner boundary is hit.""" if not valid_boundary(self.innerboundary): msg = ('Invalid boundary in multipart form:' f'{repr(self.innerboundary)}') @@ -582,7 +600,7 @@ def _skip_to_boundary(self): self.bytes_read += len(first_line) def read_multi(self): - """Internal: read a part that is itself multipart.""" + """Internal: Reads a part that is itself multipart.""" self._skip_to_boundary() max_num_fields = self.max_num_fields _list = [] @@ -634,7 +652,7 @@ def read_multi(self): return _list def read_single(self): - """Internal: read an atomic part.""" + """Internal: Reads an atomic part.""" if self.length >= 0: file = self.read_binary() self.skip_lines() @@ -644,7 +662,7 @@ def read_single(self): return file def read_binary(self): - """Internal: read binary data.""" + """Internal: Reads binary data.""" file = self.make_file() todo = self.length if todo >= 0: @@ -663,7 +681,7 @@ def read_binary(self): return file def read_lines(self): - """Internal: read lines until EOF or outerboundary.""" + """Internal: Reads lines until EOF or the outer boundary.""" if self.filename and self.file_callback: file = self.make_file() elif self.filename: @@ -678,7 +696,7 @@ def read_lines(self): return file def _write(self, line, file): - """line is always bytes, not string""" + """The line is always bytes, not a string.""" if isinstance(file, (BytesIO, StringIO)): # if file is in memory if file.tell() + len(line) > self.BUFSIZE: _file = self.make_file() @@ -692,7 +710,7 @@ def _write(self, line, file): return file def read_lines_to_eof(self, file): - """Internal: read lines until EOF.""" + """Internal: Reads lines until EOF.""" while 1: line = self.input.readline(1 << 16) self.bytes_read += len(line) @@ -703,8 +721,8 @@ def read_lines_to_eof(self, file): return file def read_lines_to_outerboundary(self, file): # noqa: C901 - """Internal: read lines until outerboundary. - Data is read as bytes: boundaries and line ends must be converted + """Internal: Reads lines until the outer boundary. + Data is read as bytes: boundaries and line endings must be converted to bytes for comparisons. """ next_boundary = b"--" + self.outerboundary @@ -754,7 +772,7 @@ def read_lines_to_outerboundary(self, file): # noqa: C901 return file def skip_lines(self): - """Internal: skip lines until outer boundary if defined.""" + """Internal: Skips lines until the outer boundary, if defined.""" if not self.outerboundary or self.done: return next_boundary = b"--" + self.outerboundary @@ -776,11 +794,10 @@ def skip_lines(self): last_line_lfend = line.endswith(b'\n') def make_file(self): - """Return readable and writable temporery file. - - If filename and file_callback was set, file_callback is called instead - of creating temporary file. + """Returns a readable and writable temporary file. + If a filename and file_callback are set, file_callback is called + instead of creating a temporary file. """ if self.filename and self.file_callback: return self.file_callback(self.filename) diff --git a/poorwsgi/headers.py b/poorwsgi/headers.py index 73193d4..5ad11b1 100644 --- a/poorwsgi/headers.py +++ b/poorwsgi/headers.py @@ -1,4 +1,4 @@ -"""Classes, which is used for managing headers. +"""Classes that are used for managing headers. :Classes: Headers :Functions: parse_negotiation, render_negotiation @@ -25,6 +25,7 @@ def _parseparam(s): + """Parse a parameter list, handling quoted strings.""" while s[:1] == ';': s = s[1:] end = s.find(';') @@ -38,9 +39,9 @@ def _parseparam(s): def parse_header(line): - """Parse a Content-type like header. + """Parses a Content-Type-like header. - Return the main content-type and a dictionary of options. + Returns the main content type and a dictionary of options. >>> parse_header("text/html; charset=latin-1") ('text/html', {'charset': 'latin-1'}) @@ -63,7 +64,8 @@ def parse_header(line): def parse_negotiation(value: str): - """Parse content negotiation headers to list of value, quality tuples. + """Parses content negotiation headers into a list of (value, quality) + tuples. >>> parse_negotiation('gzip;q=1.0, identity;q=0.5, *;q=0') [('gzip', 1.0), ('identity', 0.5), ('*', 0.0)] @@ -85,7 +87,7 @@ def parse_negotiation(value: str): def render_negotiation(negotation: List[Tuple]): - """Render negotiation header value from tuples. + """Renders a negotiation header value from tuples. >>> render_negotiation([('gzip',1.0), ('*',0)]) 'gzip;q=1.0, *;q=0' @@ -102,10 +104,10 @@ def render_negotiation(negotation: List[Tuple]): def parse_range(value: str) -> Dict[str, RangeList]: - """Parse HTTP Range header. + """Parses an HTTP Range header. - Parse `Range` header value and return dictionary with units key and list - tuples of range. + Parses the `Range` header value and returns a dictionary with the units + key and a list of range tuples. see: https://www.rfc-editor.org/rfc/rfc9110.html#name-range-requests @@ -145,7 +147,7 @@ def parse_range(value: str) -> Dict[str, RangeList]: def datetime_to_http(value: datetime): - """Return HTTP Date from timestamp. + """Returns an HTTP Date from a timestamp. >>> datetime_to_http(datetime.fromtimestamp(0, timezone.utc)) 'Thu, 01 Jan 1970 00:00:00 GMT' @@ -154,7 +156,7 @@ def datetime_to_http(value: datetime): def time_to_http(value: Optional[Union[int, float]] = None): - """Return HTTP Date from timestamp. + """Returns an HTTP Date from a timestamp. >>> time_to_http(0) 'Thu, 01 Jan 1970 00:00:00 GMT' @@ -168,7 +170,7 @@ def time_to_http(value: Optional[Union[int, float]] = None): def http_to_datetime(value: str): - """Return timestamp from HTTP Date + """Returns a datetime from an HTTP Date string. >>> http_to_datetime("Thu, 01 Jan 1970 00:00:00 GMT") datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) @@ -179,7 +181,7 @@ def http_to_datetime(value: str): def http_to_time(value: str): - """Return timestamp from HTTP Date + """Returns a timestamp from an HTTP Date. >>> http_to_time("Thu, 01 Jan 1970 00:00:00 GMT") 0 @@ -188,7 +190,7 @@ def http_to_time(value: str): class ContentRange: - """Content-Range header. + """The Content-Range header. >>> str(ContentRange(1, 2)) 'bytes 1-2/*' @@ -214,24 +216,25 @@ def __str__(self): class Headers(Mapping): - """Class inherited from collections.Mapping. + """A class inherited from collections.Mapping. - As PEP 0333, resp. RFC 2616 says, all headers names must be only US-ASCII - character except control characters or separators. And headers values must - be store in string encoded in ISO-8859-1. This class methods Headers.add - and Headers.add_header do auto convert values from UTF-8 to ISO-8859-1 - encoding if it is possible. So on every modification methods must be use - UTF-8 string. + As PEP 0333, respectively RFC 2616, states, all header names must + contain only US-ASCII characters, excluding control characters or + separators. Header values must be stored in strings encoded in + ISO-8859-1. The Headers.add and Headers.add_header methods of this + class automatically convert values from UTF-8 to ISO-8859-1 + encoding if possible. Therefore, all modification methods must use + UTF-8 strings. - Some headers can be set twice. At this moment, response can contain only - more ``Set-Cookie`` headers, but you can use add_header method to add more - headers with same name. Or you can create headers from tuples, which is - used in Request. + Some headers can be set twice. Currently, a response can contain + multiple ``Set-Cookie`` headers, but you can use the add_header + method to add multiple headers with the same name. Alternatively, + you can create headers from tuples, which is used in Request. - When more same named header is set in HTTP request, server join it's value - to one. + When multiple headers with the same name are set in an HTTP request, + the server joins their values into one. - Empty header is not allowed. + An empty header is not allowed. >>> headers = Headers({'X-Powered-By': 'Test'}) >>> headers['X-Powered-By'] @@ -247,17 +250,17 @@ class Headers(Mapping): >>> 'x-powered-by' in headers True """ - def __init__(self, headers: Optional[HeadersList] = None, strict: bool = True): """Headers constructor. - Headers object could be create from list, set or tuple of pairs - name, value. Or from dictionary. All names or values must be - iso-8859-1 encodable. If not, AssertionError will be raised. + A Headers object can be created from a list, set, or tuple of + (name, value) pairs, or from a dictionary. All names or values + must be ISO-8859-1 encodable. If not, an AssertionError will be + raised. - If strict is False, headers names and values are not encoded to - iso-8859-1. This is for input headers using only! + If strict is False, header names and values are not encoded to + ISO-8859-1. This is for input headers only. """ headers = headers or [] if isinstance(headers, (list, tuple, set)): @@ -280,11 +283,11 @@ def __init__(self, headers: Optional[HeadersList] = None, "(got {0})".format(type(headers))) def __len__(self): - """Return len of header items.""" + """Returns the number of header items.""" return len(self.__headers) def __getitem__(self, name: str): - """Return header item identified by lower name.""" + """Returns the header item identified by its lowercase name.""" name = Headers.iso88591(name.lower()) for k, val in self.__headers: if k.lower() == name: @@ -292,13 +295,13 @@ def __getitem__(self, name: str): raise KeyError("{0!r} is not registered".format(name)) def __delitem__(self, name: str): - """Delete item identied by lower name.""" + """Deletes the item identified by its lowercase name.""" name = Headers.iso88591(name.lower()) self.__headers = list(kv for kv in self.__headers if kv[0].lower() != name) def __setitem__(self, name: str, value: str): - """Delete item if exist and set it's new value.""" + """Deletes an item if it exists and sets its new value.""" del self[name] self.add_header(name, value) @@ -309,19 +312,20 @@ def __repr__(self): return "Headers(%r)" % repr(tuple(self.__headers)) def names(self): - """Return tuple of headers names.""" + """Returns a tuple of header names.""" return tuple(k for k, v in self.__headers) def keys(self): - """Alias for names method.""" + """An alias for the names method.""" return self.names() def values(self): - """Return tuple of headers values.""" + """Returns a tuple of header values.""" return tuple(v for k, v in self.__headers) def get_all(self, name: str): - """Return tuple of all values of header identified by lower name. + """Returns a tuple of all values for the header identified by its + lowercase name. >>> headers = Headers([('Set-Cookie', 'one'), ('Set-Cookie', 'two')]) >>> headers.get_all('Set-Cookie') @@ -333,11 +337,11 @@ def get_all(self, name: str): return tuple(kv[1] for kv in self.__headers if kv[0].lower() == name) def items(self): - """Return tuple of headers pairs.""" + """Returns a tuple of header (key, value) pairs.""" return tuple(self.__headers) def setdefault(self, name: str, value: str): - """Set header value if not exist, and return it's value.""" + """Sets a header value if it does not exist, and returns its value.""" res = self.get(name) if res is None: self.add_header(name, value) @@ -345,9 +349,9 @@ def setdefault(self, name: str, value: str): return res def add(self, name: str, value: str): - """Set header name to value. + """Sets a header name to a value. - Duplicate names are not allowed instead of ``Set-Cookie``. + Duplicate names are not allowed, except for ``Set-Cookie``. """ if name != "Set-Cookie" and name in self: raise KeyError("Key %s exist." % name) @@ -358,15 +362,15 @@ def add_header(self, name: str, **kwargs): """Extended header setting. - name : str - Header field to add. + name + The header field to add. - value : str or list of tuples - If value is list of tuples, render_negogation will be used. + value + If the value is a list of tuples, render_negotiation will be used. - kwargs : dict - arguments can be used to set additional value parameters for the - header field, with underscores converted to dashes. Normally the + kwargs + Arguments can be used to set additional value parameters for the + header field, with underscores converted to dashes. Normally, the parameter will be added as name="value". .. code:: python @@ -374,12 +378,11 @@ def add_header(self, name: str, h.add_header('X-Header', 'value') h.add_header('Content-Disposition', 'attachment', filename='image.png') - h.add_header('Accept-Encodding', [('gzip',1.0), ('*',0)]) + h.add_header('Accept-Encoding', [('gzip',1.0), ('*',0)]) - All names must be US-ASCII string except control characters + All names must be US-ASCII strings, excluding control characters or separators. """ - parts = [] if isinstance(value, (list, tuple)): @@ -402,10 +405,10 @@ def add_header(self, name: str, @staticmethod def iso88591(value: str) -> str: - """Doing automatic conversion to iso-8859-1 strings. + """Performs automatic conversion to ISO-8859-1 strings. - Converts from utf-8 to iso-8859-1 string. That means, all input value - of Headers class must be UTF-8 stings. + Converts from UTF-8 to ISO-8859-1 strings. This means all input values + for the Headers class must be UTF-8 strings. """ try: if isinstance(value, str): @@ -419,7 +422,7 @@ def iso88591(value: str) -> str: @staticmethod def utf8(value: str) -> str: - """Doing automatic conversion to utf-8 strings.""" + """Performs automatic conversion to UTF-8 strings.""" try: return value.encode('iso-8859-1').decode('utf-8') except UnicodeError: diff --git a/poorwsgi/openapi_wrapper.py b/poorwsgi/openapi_wrapper.py index 0f0e940..18c059f 100644 --- a/poorwsgi/openapi_wrapper.py +++ b/poorwsgi/openapi_wrapper.py @@ -1,7 +1,8 @@ """OpenAPI core wrappers module. -This module, and only this module requires ``openapi_core`` python module from -https://github.com/p1c2u/openapi-core with version 0.13.0 or higher. +This module (and only this module) requires the ``openapi_core`` Python +module from https://github.com/p1c2u/openapi-core, with version 0.13.0 +or higher. :Classes: OpenAPIRequest, OpenAPIResponse """ @@ -14,11 +15,11 @@ class OpenAPIRequest(Request): - """Wrapper of PoorWSGI request to OpenAPIRequest. + """Wrapper of a PoorWSGI request to OpenAPIRequest. - Be careful with testing of big incoming request body property, which - returns Request.data depend on ``auto_data`` and ``data_size`` - configuration properties. Request.data is available only when request + Be careful when testing large incoming request body properties, which + return Request.data depending on the ``auto_data`` and ``data_size`` + configuration properties. Request.data is available only when the request's Content-Length is available. """ re_pattern = re.compile(r"<(\w*:)?(\w*)>") @@ -28,22 +29,22 @@ def __init__(self, request): @property def host_url(self): - """Return host_url for validator.""" + """Returns the host_url for the validator.""" return self.request.construct_url('') @property def path(self): - """Return method path""" + """Returns the method path.""" return self.request.path @property def method(self): - """Return method in lower case for validator.""" + """Returns the method in lowercase for the validator.""" return self.request.method.lower() @property def full_url_pattern(self): - """Return full_url_pattern for validator.""" + """Returns the full_url_pattern for the validator.""" if self.request.uri_rule is None: return self.host_url+self.request.uri return self.host_url+OpenAPIRequest.re_pattern.sub( @@ -51,7 +52,7 @@ def full_url_pattern(self): @property def parameters(self): - """Return RequestParameters object for validator.""" + """Returns the RequestParameters object for the validator.""" path_args = OrderedDict() for (key, val) in self.request.path_args.items(): # allowed openapi core types... @@ -69,41 +70,41 @@ def parameters(self): @property def body(self): - """Return request data for validator.""" + """Returns the request data for the validator.""" return self.request.data @property def mimetype(self): - """Return request mime_type for validator.""" + """Returns the request MIME type for the validator.""" return self.request.mime_type class OpenAPIResponse(Response): - """Wrapper of PoorWSGI request to OpenAPIResponse.""" + """Wrapper of a PoorWSGI response to OpenAPIResponse.""" def __init__(self, response): self.response = response @property def data(self): - """Return response data for validator. + """Returns the response data for the validator. - Warning! This will not work for generator responses" + Warning! This will not work for generator responses. """ return self.response.data @property def status_code(self): - """Return response status_code for validator.""" + """Returns the response status_code for the validator.""" return self.response.status_code @property def mimetype(self): - """Return response mime_type for validator.""" + """Returns the response MIME type for the validator.""" return self.response.headers.get( 'Content-Type', self.response.content_type).split(';')[0] @property def headers(self): - """Return response headers for validator.""" + """Returns the response headers for the validator.""" return self.response.headers diff --git a/poorwsgi/request.py b/poorwsgi/request.py index 7f3fd68..fa4bda6 100644 --- a/poorwsgi/request.py +++ b/poorwsgi/request.py @@ -1,4 +1,4 @@ -"""Classes, which is used for managing requests. +"""Classes that are used for managing requests. :Classes: SimpleRequest, Request, EmptyForm, Args, Json """ @@ -69,34 +69,34 @@ def debug(self): @property def app(self): - """Return Application object which was created Request.""" + """Returns the Application object that created the Request.""" return self.__app @property def environ(self): - """Copy of table object containing request environment. + """Copy of the table object containing the request environment. - Information is get from wsgi server. + Information is retrieved from the WSGI server. """ return self.__environ.copy() @property def poor_environ(self): - """Environ with ``poor_`` variables. + """Environment with ``poor_`` variables. - It is environ from request, or os.environ + It is the environment from the request or os.environ. """ return self.__poor_environ.copy() @property def uri_rule(self): - """Rule from one of application handler table. + """Rule from one of the application handler tables. - This property could be set once, and that do Application object. There - are some internal uri_rules which is set typical if some internal - handler was called. There are: ``/*`` for default, directory and file - handler and ``/debug-info`` for debug handler. In other case, there be - url or regex. + This property can be set only once by the Application object. There + are some internal uri_rules that are typically set if an internal + handler was called. These include: ``/*`` for the default, + directory, and file handlers, and ``/debug-info`` for the debug + handler. In other cases, it will be a URL or a regex. """ return self.__uri_rule @@ -107,15 +107,16 @@ def uri_rule(self, value: str): @property def uri_handler(self): - """This property is set at the same point as uri_rule. + """This property is set at the same time as uri_rule. - It was set by Application object when end point handler is known before - calling all pre handlers. Typical use case is set some special - attribute to handler, and read them in pre handler. + It is set by the Application object when the endpoint handler is + known before calling all pre-handlers. A typical use case is to + set a special attribute on the handler and read it in a + pre-handler. - Property was set when any route is found for request uri. Sending file - internaly when document_root is set, or by Error handlers leave - uri_handler None. + The property is set when any route is found for the request URI. + Sending a file internally when document_root is set, or by Error + handlers, leaves uri_handler as None. """ return self.__uri_handler @@ -126,10 +127,10 @@ def uri_handler(self, value: Callable): @property def error_handler(self): - """This property is set only when error handler was called. + """This property is set only when an error handler is called. - It was set by Application object when error handler is known before - calling. + It is set by the Application object when the error handler is + known before being called. """ return self.__error_handler @@ -174,7 +175,7 @@ def uri(self): @property def path(self): - """Path part of url.""" + """Path part of the URL.""" try: return ( self.__environ.get("PATH_INFO").encode("iso-8859-1").decode() @@ -192,7 +193,7 @@ def query(self): @property def full_path(self): - """Path with query, if it exist, from url.""" + """Path with query, if it exists, from the URL.""" query = self.query return self.path + ("?" + query if query else "") @@ -208,7 +209,7 @@ def remote_addr(self): @property def referer(self): - """Request referer if is available or None.""" + """Request referer if available, otherwise None.""" return self.__environ.get("HTTP_REFERER") @property @@ -218,7 +219,7 @@ def user_agent(self): @property def server_scheme(self): - """Request scheme, typical ``http`` or ``https``.""" + """Request scheme, typically ``http`` or ``https``.""" return self.__environ.get("wsgi.url_scheme") @property @@ -236,7 +237,7 @@ def server_software(self): @property def server_admin(self): - """Server admin if set, or ``webmaster@hostname``.""" + """Server admin if set, otherwise ``webmaster@hostname``.""" return self.__environ.get("SERVER_ADMIN", f"webmaster@{self.hostname}") @property @@ -258,23 +259,24 @@ def port(self): def server_protocol(self): """Server protocol, as given by the client. - In ``HTTP/0.9``. cgi ``SERVER_PROTOCOL`` value. + In ``HTTP/1.1``. CGI ``SERVER_PROTOCOL`` value. """ return self.__environ.get("SERVER_PROTOCOL") @property def protocol(self): - """Alias for ``server_protocol`` property""" + """Alias for ``server_protocol`` property.""" return self.__environ.get("SERVER_PROTOCOL") @property def forwarded_for(self): - """``X-Forward-For`` http header if exists.""" + """The ``X-Forward-For`` HTTP header, if it exists.""" return self.__environ.get("HTTP_X_FORWARDED_FOR") @property def forwarded_host(self): - """``X-Forward-Host`` http header without port if exists.""" + """The ``X-Forwarded-Host`` HTTP header without the port, if it + exists.""" host = self.__environ.get("HTTP_X_FORWARDED_HOST") if host: host = host.split(":")[0] @@ -295,25 +297,25 @@ def forwarded_port(self): @property def forwarded_proto(self): - """``X-Forward-Proto`` http header if exists.""" + """The ``X-Forwarded-Proto`` HTTP header, if it exists.""" return self.__environ.get("HTTP_X_FORWARDED_PROTO") @property def secret_key(self): - """Value of ``poor_SecretKey`` variable. + """Value of the ``poor_SecretKey`` variable. - Secret key is used by PoorSession class. It is generate from - some server variables, and the best way is set programmatically - by Application.secret_key from random data. + The secret key is used by the PoorSession class. It is generated from + some server variables, and the best way to set it is programmatically + via Application.secret_key from random data. """ return self.__poor_environ.get("poor_SecretKey", self.__app.secret_key) @property def document_index(self): - """Value of poor_DocumentIndex variable. + """Value of the poor_DocumentIndex variable. - Variable is used to generate index html page, when poor_DocumentRoot - is set. + This variable is used to generate an index.html page when + poor_DocumentRoot is set. """ var = self.__poor_environ.get("poor_DocumentIndex") if var: @@ -329,19 +331,21 @@ def document_root(self): @property def start_time(self): - """Return timestamp when http request starts.""" + """Returns the timestamp of when the HTTP request started.""" return self.__start_time @property def end_time(self): - """Return timestamp when Request was created (end of __init__).""" + """Returns the timestamp of when the Request was created (at the end of + __init__).""" return self.__end_time def get_options(self): - """Returns dictionary with application variables from environment. + """Returns a dictionary with application variables from the + environment. - Application variables start with ``app_`` prefix, but in returned - dictionary is set without this prefix. + Application variables start with the ``app_`` prefix, but in the + returned dictionary, they are set without this prefix. .. code:: ini @@ -366,10 +370,11 @@ def get_options(self): def construct_url(self, uri: str): """This function returns a fully qualified URI string. - Url is create from the path specified by uri, using the information - stored in the request to determine the scheme, server host name - and port. The port number is not included in the string if it is the - same as the default port 80.""" + The URL is created from the path specified by the URI, using + information stored in the request to determine the scheme, server + hostname, and port. The port number is not included in the string + if it is the same as the default port (80 for http, 443 for + https).""" if not RE_HTTPURLPATTERN.match(uri): scheme = self.forwarded_proto or self.server_scheme @@ -387,17 +392,18 @@ def construct_url(self, uri: str): class Request(SimpleRequest): """HTTP request object with all server elements. - It could be compatible as soon as possible with mod_python.apache.request. + It aims to be as compatible as possible with mod_python.apache.request. Special variables for user use are prefixed with ``app_``. """ # pylint: disable=too-many-public-methods def __init__(self, environ, app): - """Object was created automatically in wsgi module. + """The object is created automatically in the wsgi module. - It's input parameters are the same, which Application object gets from - WSGI server plus file callback for auto request body parsing. + Its input parameters are the same as those that the Application + object gets from the WSGI server, plus a file callback for + automatic request body parsing. """ # pylint: disable=too-many-branches, too-many-statements super().__init__(environ, app) @@ -495,32 +501,32 @@ def __init__(self, environ, app): # pylint: disable=invalid-name self._SimpleRequest__end_time = time() - # enddef - # -------------------------- Properties --------------------------- # @property def mime_type(self) -> str: - """Request ``Content-Type`` header or empty string if not set.""" + """The request's ``Content-Type`` header, or an empty string if not + set.""" return self.__mime_type @property def charset(self) -> str: - """Request ``Content-Type`` charset header string, utf-8 if not set.""" + """The request's ``Content-Type`` charset header string; defaults to + 'utf-8' if not set.""" return self.__charset @property def content_length(self) -> int: - """Request ``Content-Length`` header value, -1 if not set.""" + """The request's ``Content-Length`` header value; -1 if not set.""" return self.__content_length @property def headers(self): - """Reference to input headers object.""" + """A reference to the input headers object.""" return self.__headers @property def accept(self) -> tuple: - """Tuple of client supported mime types from Accept header.""" + """A tuple of client-supported MIME types from the Accept header.""" if self.__accept is None: self.__accept = tuple( parse_negotiation(self.__headers.get("Accept", "")) @@ -529,7 +535,8 @@ def accept(self) -> tuple: @property def accept_charset(self) -> tuple: - """Tuple of client supported charset from Accept-Charset header.""" + """A tuple of client-supported charsets from the Accept-Charset + header.""" if self.__accept_charset is None: self.__accept_charset = tuple( parse_negotiation(self.__headers.get("Accept-Charset", "")) @@ -538,7 +545,8 @@ def accept_charset(self) -> tuple: @property def accept_encoding(self) -> tuple: - """Tuple of client supported charset from Accept-Encoding header.""" + """A tuple of client-supported encodings from the Accept-Encoding + header.""" if self.__accept_encoding is None: self.__accept_encoding = tuple( parse_negotiation(self.__headers.get("Accept-Encoding", "")) @@ -547,7 +555,8 @@ def accept_encoding(self) -> tuple: @property def accept_language(self) -> tuple: - """List of client supported languages from Accept-Language header.""" + """A tuple of client-supported languages from the Accept-Language + header.""" if self.__accept_language is None: self.__accept_language = tuple( parse_negotiation(self.__headers.get("Accept-Language", "")) @@ -556,28 +565,28 @@ def accept_language(self) -> tuple: @property def accept_html(self) -> bool: - """Return true if ``text/html`` mime type is in accept negotiations - values. + """Returns True if the ``text/html`` MIME type is in the accepted + negotiation values. """ return "text/html" in dict(self.accept) @property def accept_xhtml(self) -> bool: - """Return true if ``text/xhtml`` mime type is in accept negotiations - values. + """Returns True if the ``text/xhtml`` MIME type is in the accepted + negotiation values. """ return "text/xhtml" in dict(self.accept) @property def accept_json(self) -> bool: - """Return true if ``application/json`` mime type is in accept - negotiations values. + """Returns True if the ``application/json`` MIME type is in the + accepted negotiation values. """ return "application/json" in dict(self.accept) @property def authorization(self) -> dict: - """Return Authorization header parsed to dictionary.""" + """Returns the Authorization header parsed into a dictionary.""" if self.__authorization is None: auth = self.__headers.get("Authorization", "").strip() self.__authorization = dict( @@ -592,18 +601,19 @@ def authorization(self) -> dict: @property def is_xhr(self) -> bool: - """If ``X-Requested-With`` header is set with ``XMLHttpRequest`` value. + """Returns True if the ``X-Requested-With`` header is set to + ``XMLHttpRequest``. """ return self.__headers.get("X-Requested-With") == "XMLHttpRequest" @property def is_body_request(self) -> bool: - """True if has set Content-Length more than zero.""" + """Returns True if Content-Length is greater than zero.""" return self.__content_length > 0 @property def is_chunked(self) -> bool: - """True if has set Transfer-Encoding is chunked.""" + """Returns True if Transfer-Encoding is 'chunked'.""" return self.__headers.get("Transfer-Encoding") == "chunked" @property @@ -618,7 +628,8 @@ def is_chunked_request(self): @property def path_args(self) -> dict: - """Dictionary arguments from path of regual expression rule.""" + """A dictionary of arguments from the path of a regular expression + rule.""" return (self.__path_args or {}).copy() @path_args.setter @@ -628,13 +639,13 @@ def path_args(self, value: dict): @property def args(self): - """Extended dictionary (Args instance) of request arguments. + """An extended dictionary (Args instance) of request arguments. - Argument are parsed from QUERY_STRING, which is typical, but not only - for GET method. Arguments are parsed when Application.auto_args is set - which is default. + Arguments are parsed from the QUERY_STRING, which is typical for, + but not limited to, the GET method. Arguments are parsed when + Application.auto_args is set (which is the default). - This property could be **set only once**. + This property can be **set only once**. """ return self.__args @@ -645,14 +656,14 @@ def args(self, value: "Args"): @property def form(self): - """Dictionary like class (FieldStorage instance) of body arguments. + """A dictionary-like class (FieldStorage instance) for body arguments. - Arguments must be send in request body with mime type - one of Application.form_mime_types. Method must be POST, PUT - or PATCH. Request body is parsed when Application.auto_form - is set, which default and when method is POST, PUT or PATCH. + Arguments must be sent in the request body with a MIME type from + Application.form_mime_types. The method must be POST, PUT, + or PATCH. The request body is parsed when Application.auto_form + is set (which is the default) and the method is POST, PUT, or PATCH. - This property could be **set only once**. + This property can be **set only once**. """ return self.__form @@ -663,34 +674,36 @@ def form(self, value: fieldstorage.FieldStorage): @property def json(self): - """Json dictionary if request mime type is JSON. + """A JSON dictionary if the request's MIME type is JSON. - Json types is defined in Application.json_mime_types, typical is - ``application/json`` and request method must be POST, PUT or PATCH and - Application.auto_json must be set to true (default). Otherwise json - is EmptyForm. + JSON types are defined in Application.json_mime_types (typically + ``application/json``). The request method must be POST, PUT, or + PATCH, and Application.auto_json must be set to True (default). + Otherwise, json is an EmptyForm. - When request data is present, that will by parsed with + When request data is present, it will be parsed with the parse_json_request function. """ return self.__json @property def cookies(self): - """SimpleCookie iterable object of all cookies from Cookie header. + """A SimpleCookie iterable object of all cookies from the Cookie + header. - This property was set if Application.auto_cookies is set to true, - which is default. Otherwise cookies is None. + This property is set if Application.auto_cookies is set to True + (which is the default). Otherwise, cookies is None. """ return self.__cookies @property def data(self): # pylint: disable=inconsistent-return-statements - """Returns input data from wsgi.input file. + """Returns input data from the wsgi.input file. - This works only, when auto_data configuration and Content-Length of - request are lower then input_cache configuration value. Other requests - like big file data uploads increase memory and time system requests. + This works only when auto_data is configured and the request's + Content-Length is lower than the input_cache configuration value. + Other requests, like large file data uploads, will increase + memory and system request time. """ if isinstance(self.__file, BytesIO): try: @@ -701,7 +714,7 @@ def data(self): # pylint: disable=inconsistent-return-statements @property def input(self): - """Return input file, for internal use in FieldStorage""" + """Returns the input file; for internal use in FieldStorage.""" if self.__cached_input: return self.__cached_input if not self.__cached_size or isinstance(self.__file, BytesIO): @@ -716,7 +729,7 @@ def input(self): @property def user(self): - """For user object, who is login for example (default None).""" + """For the user object, e.g., who is logged in (defaults to None).""" return self.__user @user.setter @@ -725,7 +738,7 @@ def user(self, value): @property def api(self): - """For api request object, could be used for OpenAPIRequest.""" + """For the API request object; can be used for OpenAPIRequest.""" return self.__api @api.setter @@ -734,7 +747,8 @@ def api(self, value): @property def db(self): - """For api request object, could be used for database connection(s).""" + """For the API request object; can be used for database + connection(s).""" return self.__db @db.setter @@ -746,10 +760,10 @@ def __read(self, length: int = -1): return self.__file.read(length) def read(self, length=-1): # pylint: disable=method-hidden - """Read data from client (typical for XHR2 data POST). + """Reads data from the client (typical for XHR2 data POST). - If length is not set, or if is lower then zero, Content-Length was - be use. + If length is not set, or if it is less than zero, Content-Length will + be used. """ if not self.is_body_request and self.server_protocol != "HTTP/0.9": log.error("No Content-Length found, read was failed!") @@ -760,13 +774,14 @@ def read(self, length=-1): # pylint: disable=method-hidden return self.__file.read(self.__content_length) def read_chunk(self): - """Read chunk when Transfer-Encoding is `chunked`. + """Reads a chunk when Transfer-Encoding is 'chunked'. - Method read line with chunk size first, then read chunk and return it, - or raise ValueError when chunk size is in bad format. + The method first reads a line with the chunk size, then reads the + chunk and returns it. It will raise a ValueError if the chunk size + is in a bad format. - Be sure that wsgi server allow readline from wsgi.input. For examples - uWSGI has extra API + Ensure that the WSGI server allows readline from wsgi.input. For + example, uWSGI has an extra API for this: https://uwsgi-docs.readthedocs.io/en/latest/Chunked.html """ size = int(self.__file.readline(), base=16) @@ -786,7 +801,7 @@ class EmptyForm(dict, fieldstorage.FieldStorageInterface): def getvalue( self, key: str, default: Any = None, func: Callable = lambda x: x ): - """Just return default.""" + """Simply returns the default value.""" return default def getfirst( @@ -796,7 +811,7 @@ def getfirst( func: Callable = lambda x: x, fce: Optional[Callable] = None, ): - """Just return default.""" + """Simply returns the default value.""" if fce: warnings.warn( "Using deprecated fce argument. Use func instead.", @@ -812,7 +827,7 @@ def getlist( func: Callable = lambda x: x, fce: Optional[Callable] = None, ): - """Just return default or empty list.""" + """Simply returns the default value or an empty list.""" if fce: warnings.warn( "Using deprecated fce argument. Use func instead.", @@ -823,10 +838,10 @@ def getlist( class Args(dict, fieldstorage.FieldStorageInterface): - """Compatibility class for read values from QUERY_STRING. + """Compatibility class for reading values from QUERY_STRING. - Class is based on dictionary. It has getfirst and getlist methods, - which can call function on values. + This class is based on a dictionary. It has getfirst and getlist methods, + which can call a function on the values. """ def __init__(self, req: Request, keep_blank_values=0, strict_parsing=0): @@ -844,12 +859,13 @@ def __init__(self, req: Request, keep_blank_values=0, strict_parsing=0): class JsonDict(dict, fieldstorage.FieldStorageInterface): - """Compatibility class for read values from JSON POST, PUT or PATCH + """A compatibility class for reading values from a JSON POST, PUT, or PATCH request. - It has getfirst and getlist methods, which can call function on values. + It has getfirst and getlist methods, which can call a function on the + values. - **Deprecated:** this class will be deleted in next major version. + **Deprecated:** This class will be removed in a future major version. >>> json = JsonDict({"key": "42"}) >>> json.getvalue("key", func=int) @@ -865,27 +881,28 @@ class JsonDict(dict, fieldstorage.FieldStorageInterface): class JsonList(list): - """Compatibility class for read values from JSON POST, PUT or PATCH + """A compatibility class for reading values from a JSON POST, PUT, or PATCH request. - It has getfirst and getlist methods, which can call function on values. + It has getfirst and getlist methods, which can call a function on the + values. - **Deprecated:** this class will be deleted in next major version. + **Deprecated:** This class will be removed in a future major version. """ # pylint: disable=unused-argument def getvalue( self, key=None, default: Any = None, func: Callable = lambda x: x ): - """Returns first item or default if no exists. - - key : None - Compatibility parametr is ignored. - default : any - Default value if key not exists. - func : converter (lambda x: x) - Function or class which processed value. Default type of value - is bytes for files and string for others. + """Returns the first item, or the default value if it does not exist. + + key + This compatibility parameter is ignored. + default + The default value if the key does not exist. + func + A function or class that processes the value. The default + type of the value is bytes for files and string for others. """ return func(self[0]) if self else default @@ -896,16 +913,18 @@ def getfirst( func: Callable = lambda x: x, fce: Optional[Callable] = None, ): - """Returns first variable value or default, if no one exist. - - key : None - Compatibility parametr is ignored. - default : any - Default value if key not exists. - func : converter - Function which processed value. - fce : deprecated converter name - Use func converter just like getvalue. + """Returns the first variable's value, or the default if it does not + exist. + + key + This compatibility parameter is ignored. + default + The default value if the key does not exist. + func + A function that processes the value. + fce + Deprecated converter name. Use the func converter just like + getvalue. """ if fce: warnings.warn( @@ -924,16 +943,17 @@ def getlist( func: Callable = lambda x: x, fce: Optional[Callable] = None, ): - """Returns list of values - - key : None - Compatibility parametr is ignored. - default : list - Default value when self is empty. - func : converter - Function which processed value. - fce : deprecated converter name - Use func converter just like getvalue. + """Returns a list of values. + + key + This compatibility parameter is ignored. + default + The default value when self is empty. + func + A function that processes the value. + fce + Deprecated converter name. Use the func converter just like + getvalue. """ if fce: warnings.warn( @@ -950,15 +970,15 @@ def getlist( # pylint: disable=inconsistent-return-statements def parse_json_request(raw: bytes, charset: str = "utf-8"): - """Try to parse request data. + """Tries to parse request data. - Returned type could be: + The returned type can be: - * JsonDict when dictionary is parsed. - * JsonList when list is parsed. - * Other based types from json.loads function like str, int, float, bool - or None. - * None when parsing of JSON fails. That is logged with WARNING log level. + * JsonDict, when a dictionary is parsed. + * JsonList, when a list is parsed. + * Other base types from the json.loads function, such as str, int, + float, bool, or None. + * None, when JSON parsing fails. This is logged with a WARNING log level. """ # pylint: disable=inconsistent-return-statements @@ -985,11 +1005,11 @@ def FieldStorage( # noqa: N802 separator="&", file_callback=None, ): - """**Deprecated:** back compatibility function. + """**Deprecated:** A backwards compatibility function. - This function will be deleted in next major version. + This function will be removed in a future major version. - Use direct FieldStorageParser instead of this!. + Use FieldStorageParser directly instead of this. """ # pylint: disable=unused-argument # pylint: disable=invalid-name @@ -1017,10 +1037,10 @@ def FieldStorage( # noqa: N802 class CachedInput: """ - Wrapper around wsgi.input file, which reads data block by block. + A wrapper around the wsgi.input file that reads data block by block. - timeout : float - how long to wait for new bytes in seconds + timeout + How long to wait for new bytes, in seconds. """ def __init__( @@ -1033,7 +1053,7 @@ def __init__( self.block_size = block_size def read(self, size=-1): - """Compatible file read which works with internal buffer.""" + """A compatible file read that works with an internal buffer.""" if size < 0: size = self.block_size @@ -1056,7 +1076,7 @@ def read(self, size=-1): return self.__file.read(size) def readline(self, size=-1): # noqa: C901 - """Compatible file read which works with internal buffer.""" + """A compatible file read that works with an internal buffer.""" if size < 0: size = self.block_size @@ -1075,7 +1095,7 @@ def readline(self, size=-1): # noqa: C901 max_size = size - l_size pos = self.__buffer.find(b"\r\n", 0, max_size) if pos >= 0: - line += self.__buffer[:pos + 2] + line += self.__buffer[: pos + 2] self.__buffer = self.__buffer[pos + 2:] return line diff --git a/poorwsgi/response.py b/poorwsgi/response.py index 9836d7e..195be94 100644 --- a/poorwsgi/response.py +++ b/poorwsgi/response.py @@ -6,78 +6,95 @@ StrGeneratorResponse, EmptyResponse, RedirectResponse :Functions: make_response, redirect, abort """ -from http.client import responses -from io import BytesIO, IOBase, BufferedIOBase, TextIOBase -from os import access, R_OK, fstat -from os.path import getctime -from logging import getLogger -from json import dumps -from inspect import stack -from datetime import datetime -from typing import Union, Callable, Iterable, BinaryIO, Optional import mimetypes +from datetime import datetime +from http.client import responses +from inspect import stack +from io import BufferedIOBase, BytesIO, IOBase, TextIOBase +from json import dumps +from logging import getLogger +from os import R_OK, access, fstat +from os.path import getctime +from typing import BinaryIO, Callable, Iterable, Optional, Union try: from simplejson import JSONEncoder + JSON_GENERATOR = True except ImportError: JSON_GENERATOR = False +from poorwsgi.headers import ( + ContentRange, + Headers, + HeadersList, + RangeList, + datetime_to_http, + time_to_http, +) from poorwsgi.state import ( DECLINED, - HTTP_OK, - HTTP_NO_CONTENT, - HTTP_PARTIAL_CONTENT, + HTTP_I_AM_A_TEAPOT, HTTP_MOVED_PERMANENTLY, HTTP_MOVED_TEMPORARILY, - HTTP_I_AM_A_TEAPOT, + HTTP_NO_CONTENT, HTTP_NOT_MODIFIED, + HTTP_OK, + HTTP_PARTIAL_CONTENT, HTTP_RANGE_NOT_SATISFIABLE, - deprecated + deprecated, ) -from poorwsgi.headers import Headers, HeadersList, RangeList, \ - time_to_http, datetime_to_http, ContentRange -log = getLogger('poorwsgi') +log = getLogger("poorwsgi") # not in http.client.responses responses[HTTP_I_AM_A_TEAPOT] = "I'm a teapot" # pylint: disable=unsubscriptable-object # pylint: disable=consider-using-f-string +# pylint: disable=too-many-lines NOT_MODIFIED_DENY = { - 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', - 'Content-Range', 'Content-Type'} -NOT_MODIFIED_ONE_OF_REQUIRED = { - 'Content-Location', 'Date', 'ETag', 'Vary' - } + "Content-Encoding", + "Content-Language", + "Content-Length", + "Content-MD5", + "Content-Range", + "Content-Type", +} +NOT_MODIFIED_ONE_OF_REQUIRED = {"Content-Location", "Date", "ETag", "Vary"} class IBytesIO(BytesIO): - """Class for returning bytes when is iterate.""" + """Class for returning bytes when iterated.""" def read_kilo(self): - """Read 1024 bytes from buffer.""" + """Reads 1024 bytes from the buffer.""" return self.read(1024) def __iter__(self): - """Iterate object by 1024 bytes.""" - return iter(self.read_kilo, b'') + """Iterates over the object in 1024-byte chunks.""" + return iter(self.read_kilo, b"") class BaseResponse: - """Base class for response.""" + """The base class for a response.""" + _ranges: RangeList _units: Optional[str] - def __init__(self, content_type: str = "", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK): - assert isinstance(content_type, str), \ + def __init__( + self, + content_type: str = "", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + ): + assert isinstance(content_type, str), ( "content_type is not string but `%s`" % content_type - assert isinstance(status_code, int), \ + ) + assert isinstance(status_code, int), ( "status_code is not number but `%s`" % status_code + ) # String. The content type. Another way to set content_type is via # headers_out object property. Default is text/html; charset=utf-8 @@ -88,7 +105,8 @@ def __init__(self, content_type: str = "", self.__headers = headers elif headers is None: self.__headers = Headers( - (("X-Powered-By", "Poor WSGI for Python"),)) + (("X-Powered-By", "Poor WSGI for Python"),) + ) else: self.__headers = Headers(headers) @@ -104,11 +122,11 @@ def __init__(self, content_type: str = "", @property def status_code(self): - """Http status code, which is **state.HTTP_OK (200)** by default. + """The HTTP status code, which is **state.HTTP_OK (200)** by default. - If you want to set this variable (which is very good idea in http_state - handlers), it is good solution to use some of ``HTTP_`` constant from - state module. + If you want to set this variable (which is a very good idea in + http_state handlers), it is a good solution to use one of the + ``HTTP_`` constants from the state module. """ return self.__status_code @@ -119,41 +137,44 @@ def status_code(self, value: int): if value not in (HTTP_OK, HTTP_PARTIAL_CONTENT) and self._ranges: stack_record = stack()[1] # pylint: disable=logging-format-interpolation - log.warning("%s status code can't be partial.\n" - " File {1}, line {2}, in {3} \n" - "{0}".format((stack_record[4] or [''])[0], - *stack_record[1:4]), - self.__status_code) + log.warning( + "%s status code can't be partial.\n" + " File {1}, line {2}, in {3} \n" + "{0}".format((stack_record[4] or [""])[0], *stack_record[1:4]), + self.__status_code, + ) self._ranges.clear() - del self.__headers['Accept-Ranges'] + del self.__headers["Accept-Ranges"] self.__status_code = value self.__reason = responses[self.__status_code] @property def reason(self): - """HTTP response is set automatically with setting status_code. + """The HTTP response reason phrase is set automatically when setting + the status_code. - Setting response message is not good idea, but you can create - own class based on Response, when you can override status_code setter. + Modifying the response message is not a good idea, but you can create + your own class based on Response, where you can override the + status_code setter. """ return self.__reason @property def content_length(self): - """Return content_length of response. + """Returns the content_length of the response. - That is size of internal buffer. + This is the size of the internal buffer. """ return self._content_length @property def data(self): - """Return data content.""" - return b'' + """Returns the data content.""" + return b"" @property def headers(self): - """Reference to output headers object.""" + """A reference to the output headers object.""" return self.__headers @headers.setter @@ -164,23 +185,25 @@ def headers(self, value: Union[Headers, HeadersList]): self.__headers = Headers(value) def add_header(self, name: str, value: str, **kwargs): - """Call Headers.add_header on headers object.""" + """Calls Headers.add_header on the headers object.""" self.__headers.add_header(name, value, **kwargs) def make_partial(self, ranges: Optional[RangeList] = None, units="bytes"): - """Make response partial. + """Makes the response partial. - It adds `Accept-Ranges` headers with units value and set range to new - value. If range is defined, and response support seek in buffer, or - skip generator, it returns right range response. + It adds the `Accept-Ranges` header with the units value and sets + the range to a new value. If a range is defined and the response + supports seeking in the buffer or skipping the generator, it + returns the correct range response. Inconsistent ranges are skipped! - Response status_code **MUST** be HTTP_OK (200 OK). **Only one range** - is supported at this moment. Other behaviour, like `If-Range` - conditions depends on response or programmers support. + The response status_code **MUST** be HTTP_OK (200 OK). **Only + one range** is supported at this moment. Other behavior, like + `If-Range` conditions, depends on the response or programmer's + implementation. - see https://www.rfc-editor.org/rfc/rfc9110.html#name-range-requests + See https://www.rfc-editor.org/rfc/rfc9110.html#name-range-requests >>> res = BaseResponse() >>> res.make_partial([(0, 100)]) @@ -193,15 +216,16 @@ def make_partial(self, ranges: Optional[RangeList] = None, units="bytes"): if self.__status_code != HTTP_OK: stack_record = stack()[1] # pylint: disable=logging-format-interpolation - log.warning("%s status code can't be partial.\n" - " File {1}, line {2}, in {3} \n" - "{0}".format((stack_record[4] or [''])[0], - *stack_record[1:4]), - self.__status_code) + log.warning( + "%s status code can't be partial.\n" + " File {1}, line {2}, in {3} \n" + "{0}".format((stack_record[4] or [""])[0], *stack_record[1:4]), + self.__status_code, + ) return self._units = units - self.add_header('Accept-Ranges', units) + self.add_header("Accept-Ranges", units) self._ranges.clear() for start, end in ranges or []: if end is not None and start is not None and end < start: @@ -210,20 +234,22 @@ def make_partial(self, ranges: Optional[RangeList] = None, units="bytes"): self._ranges.append((start, end)) def make_range(self, ranges: RangeList, units="bytes", full="*"): - """Just set Content-Range header values and units attribute. + """Just sets the Content-Range header values and the units attribute. - Content-Range is set in __start_response__ method for HTTP_OK status. - This method is need, when you want to make partial response by - yourself. This method needs response with HTTP_PARTIAL_CONTENT status. + The Content-Range is set in the __start_response__ method for an + HTTP_OK status. This method is needed when you want to create a + partial response manually. This method requires a response + with an HTTP_PARTIAL_CONTENT status. """ if self.__status_code != HTTP_PARTIAL_CONTENT: stack_record = stack()[1] # pylint: disable=logging-format-interpolation - log.warning("%s status code can't be partial.\n" - " File {1}, line {2}, in {3} \n" - "{0}".format((stack_record[4] or [''])[0], - *stack_record[1:4]), - self.__status_code) + log.warning( + "%s status code can't be partial.\n" + " File {1}, line {2}, in {3} \n" + "{0}".format((stack_record[4] or [""])[0], *stack_record[1:4]), + self.__status_code, + ) return self._units = units @@ -238,14 +264,14 @@ def make_range(self, ranges: RangeList, units="bytes", full="*"): if len(ranges) != 1: log.warning("Only one range will be used!") if len(ranges) >= 1: - del self.headers['Content-Range'] + del self.headers["Content-Range"] start, end = self.ranges[0] content_range = ContentRange(start, end, full, units) self.headers.add("Content-Range", str(content_range)) @property def ranges(self): - """Tuple of ranges set in make_partial method.""" + """A tuple of ranges set in the make_partial method.""" return tuple(self._ranges) def __start_response__(self, start_response: Callable): # noqa: C901 @@ -257,17 +283,19 @@ def __start_response__(self, start_response: Callable): # noqa: C901 _headers = set(self.__headers.keys()) if _headers.intersection(NOT_MODIFIED_DENY): log.warning( - 'Some representation header in Not Modified response') + "Some representation header in Not Modified response" + ) if not _headers.intersection(NOT_MODIFIED_ONE_OF_REQUIRED): log.warning( - 'Missing any required header in Not Modified response') + "Missing any required header in Not Modified response" + ) else: if self.__status_code == HTTP_OK: if self._ranges and self._units == "bytes": - del self.__headers['Accept-Ranges'] + del self.__headers["Accept-Ranges"] content_range = ContentRange( - end=self.content_length-1, - full=self._content_length) + end=self.content_length - 1, full=self._content_length + ) self._start, self._end = self.ranges[0] if self._start is None and self._content_length: self._end = min(self._content_length, self._end) @@ -275,7 +303,7 @@ def __start_response__(self, start_response: Callable): # noqa: C901 self._end = None content_range.start = self._start if self._end and self._content_length: - self._end = min(self._content_length-1, self._end) + self._end = min(self._content_length - 1, self._end) content_range.end = self._end self._content_length = self._end - self._start + 1 elif self._content_length: @@ -284,43 +312,47 @@ def __start_response__(self, start_response: Callable): # noqa: C901 self.status_code = HTTP_PARTIAL_CONTENT self.__headers.add("Content-Range", str(content_range)) # Content-Lenght header must be modified - self.__headers["Content-Length"] = \ - str(self._content_length) + self.__headers["Content-Length"] = str( + self._content_length + ) else: content_range.start, content_range.end = self.ranges[0] error = Response( - headers={"Content-Range": str(content_range)}, - status_code=HTTP_RANGE_NOT_SATISFIABLE) + headers={"Content-Range": str(content_range)}, + status_code=HTTP_RANGE_NOT_SATISFIABLE, + ) raise HTTPException(error) elif self._ranges: - log.warning("Unknown units `%s', full response will be " - "returned.", self._units) + log.warning( + "Unknown units `%s', full response will be returned.", + self._units, + ) - if self.content_type \ - and not self.__headers.get('Content-Type'): - self.__headers.add('Content-Type', self.content_type) + if self.content_type and not self.__headers.get("Content-Type"): + self.__headers.add("Content-Type", self.content_type) - if self.content_length \ - and not self.__headers.get('Content-Length'): - self.__headers.add('Content-Length', - str(self.content_length)) + if self.content_length and not self.__headers.get( + "Content-Length" + ): + self.__headers.add("Content-Length", str(self.content_length)) start_response( "%d %s" % (self.__status_code, self.__reason), - list(self.__headers.items())) + list(self.__headers.items()), + ) def __end_of_response__(self): """Method **for internal use only!**. - This method was called from Application object at the end of request - for returning right value to wsgi server. + This method is called from the Application object at the end of + the request to return the correct value to the WSGI server. """ # pylint: disable=no-self-use - return b'' + return b"" def __call__(self, start_response: Callable): if self.__done: - raise RuntimeError('Response can be used only once!') + raise RuntimeError("Response can be used only once!") try: self.__start_response__(start_response) return self.__end_of_response__() @@ -329,21 +361,27 @@ def __call__(self, start_response: Callable): class Response(BaseResponse): - """HTTP Response object. + """An HTTP Response object. - This is base Response object which is process with PoorWSGI application. + This is the base Response object that is processed by the PoorWSGI + application. - As Response uses BytesIO as internal cache, which is closed by WSGI - server, **response can be used only once!**. + Since Response uses BytesIO as an internal cache, which is closed + by the WSGI server, the **response can be used only once!**. """ + __buffer: BufferedIOBase - def __init__(self, data: Union[str, bytes] = b'', - content_type: str = "text/html; charset=utf-8", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK): - assert isinstance(data, (str, bytes)), \ + def __init__( + self, + data: Union[str, bytes] = b"", + content_type: str = "text/html; charset=utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + ): + assert isinstance(data, (str, bytes)), ( "data is not string or bytes but %s" % type(data) + ) super().__init__(content_type, headers, status_code) @@ -360,9 +398,9 @@ def data(self): return self.__buffer.read() def write(self, data: Union[str, bytes]): - """Write data to internal buffer.""" + """Writes data to the internal buffer.""" if isinstance(data, str): - data = data.encode('utf-8') + data = data.encode("utf-8") self._content_length += len(data) self.__buffer.write(data) @@ -374,10 +412,10 @@ def __end_of_response__(self): class PartialResponse(Response): - """Partial Response object which only compute Content-Range header. + """A Partial Response object that only computes the Content-Range header. - This is for special cases, when you can know how to return right range, for - example, when you want to return another unit. + This is for special cases where you know how to return the correct range, + for example, when you want to return a different unit. >>> res = PartialResponse() >>> res.make_range([(1, 3)], "blocks") @@ -387,27 +425,31 @@ class PartialResponse(Response): >>> res.headers Headers("...('Content-Range', 'blocks 1-3/10'))") """ + full: Union[str, int] - def __init__(self, data: Union[str, bytes] = b'', - content_type: str = "text/html; charset=utf-8", - headers: Optional[Union[Headers, HeadersList]] = None): + def __init__( + self, + data: Union[str, bytes] = b"", + content_type: str = "text/html; charset=utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + ): super().__init__(data, content_type, headers, HTTP_PARTIAL_CONTENT) def make_partial(self, ranges: Optional[RangeList] = None, units="bytes"): - """This mathod do nothing. + """This method does nothing. - For creating Content-Range header, use special make_range method. + To create a Content-Range header, use the special make_range method. """ log.warning("PartialResponse is partial yet. Use make_range method.") class JSONResponse(Response): - """Simple application/json response. + """A simple application/json response. Arguments: data\\_ : Any - Alternative way to add any data to json response. + An alternative way to add data to the JSON response. charset : str ``charset`` value for ``Content-Type`` header. ``utf-8`` by default. @@ -417,7 +459,7 @@ class JSONResponse(Response): HTTP Status response code, 200 (``HTTP_OK``) by default. encoder_kwargs : dict Keyword arguments for ``json.dumps`` function. - kwargs : keywords arguments + kwargs : keyword arguments Other keys and values are serialized to JSON structure. >>> res = JSONResponse(msg="Čeština", @@ -426,94 +468,109 @@ class JSONResponse(Response): b'{"msg": "\xc4\x8ce\xc5\xa1tina"}' """ - def __init__(self, data_=None, charset: str = "utf-8", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK, encoder_kwargs=None, - **kwargs): + def __init__( + self, + data_=None, + charset: str = "utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + encoder_kwargs=None, + **kwargs, + ): content_type = "application/json" encoder_kwargs = encoder_kwargs or {} if charset: - content_type += "; charset="+charset + content_type += "; charset=" + charset if kwargs and data_ is not None: raise RuntimeError("Only one of data and kwargs is allowed.") if kwargs and data_ is None: data_ = kwargs - super().__init__(dumps(data_, **encoder_kwargs), - content_type, headers, status_code) + super().__init__( + dumps(data_, **encoder_kwargs), content_type, headers, status_code + ) class TextResponse(Response): - """Simple text/plain response.""" - - def __init__(self, text: str, charset: str = "utf-8", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK): + """A simple text/plain response.""" + + def __init__( + self, + text: str, + charset: str = "utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + ): content_type = "text/plain" if charset: - content_type += "; charset="+charset + content_type += "; charset=" + charset super().__init__(text, content_type, headers, status_code) class FileObjResponse(BaseResponse): - """FileResponse returns file object direct to WSGI server. + """FileResponse returns a file object directly to the WSGI server. - This means, that sendfile UNIX system call can be used. + This means that the sendfile UNIX system call can be used. - Be careful not to use a single FileReponse instance multiple times! - WSGI server closes file, which is returned by this response. So just - like Response, instance of FileResponse can be used only once! + Be careful not to use a single FileResponse instance multiple times! + The WSGI server closes the file that is returned by this response. So, just + like Response, an instance of FileResponse can be used only once! - File content is returned from current position. So Content-Length is set - from file system or from buffer, but minus position. + The file content is returned from the current position. So, + Content-Length is set from the file system or from the buffer, + minus the position. """ - def __init__(self, file_obj: Union[IOBase, BinaryIO], - content_type: Optional[str] = None, - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK): + def __init__( + self, + file_obj: Union[IOBase, BinaryIO], + content_type: Optional[str] = None, + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + ): assert file_obj.readable() - assert not isinstance(file_obj, TextIOBase), \ + assert not isinstance(file_obj, TextIOBase), ( "file_obj must be binary stream" - if content_type is None: # default mime type + ) + if content_type is None: # default mime type content_type = "application/octet-stream" - super().__init__(content_type=content_type, - headers=headers, - status_code=status_code) + super().__init__( + content_type=content_type, headers=headers, status_code=status_code + ) self.__file = file_obj if file_obj.seekable(): self.__pos = file_obj.tell() self._start = self.__pos try: - self._content_length = \ - fstat(file_obj.fileno()).st_size - self.__pos + self._content_length = ( + fstat(file_obj.fileno()).st_size - self.__pos + ) except OSError: if isinstance(file_obj, BytesIO): - self._content_length = \ - file_obj.getbuffer().nbytes - self.__pos + self._content_length = file_obj.getbuffer().nbytes - self.__pos else: self._content_length = 0 - log.debug('File object has unknown size.') + log.debug("File object has unknown size.") # must be redefined, because self.__buffer is private attribute @property def data(self): - """Return data content. + """Returns the data content. This property works only if file_obj is seekable. """ if self.__file.seekable(): self.__file.seek(self.__pos) return self.__file.read() - log.info('File object is not seekable.') - return b'' + log.info("File object is not seekable.") + return b"" # must be redefined, because self.__buffer is private attribute def __end_of_response__(self): """Method **for internal use only!**. - This method was called from Application object at the end of request - for returning right value to wsgi server. + This method is called from the Application object at the end of + the request to return the correct value to the WSGI server. """ if self.__file.seekable(): self.__file.seek(self._start) @@ -523,52 +580,62 @@ def __end_of_response__(self): class FileResponse(FileObjResponse): - """FileResponse returns opened file direct to WSGI server. + """FileResponse returns an opened file directly to the WSGI server. - This means, that sendfile UNIX system call can be used. + This means that the sendfile UNIX system call can be used. - Be careful not to use a single FileReponse instance multiple times! - WSGI server closes file, which is returned by this response. So just - like Response, instance of FileResponse can be used only once! + Be careful not to use a single FileResponse instance multiple times! + The WSGI server closes the file that is returned by this response. So, just + like Response, an instance of FileResponse can be used only once! - This object adds Last-Modified header, if is not set. + This object adds a Last-Modified header if it is not already set. """ - def __init__(self, path: str, content_type: Optional[str] = None, - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK): + def __init__( + self, + path: str, + content_type: Optional[str] = None, + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + ): if not access(path, R_OK): raise IOError("Could not stat file for reading") - if content_type is None: # auto mime type select + if content_type is None: # auto mime type select # pylint: disable=unused-variable - (content_type, encoding) = mimetypes.guess_type(path) + (content_type, _) = mimetypes.guess_type(path) # pylint: disable=consider-using-with - super().__init__(open(path, 'rb', buffering=0), - content_type=content_type, - headers=headers, - status_code=status_code) + super().__init__( + open(path, "rb", buffering=0), + content_type=content_type, + headers=headers, + status_code=status_code, + ) self.make_partial() - if 'Last-Modified' not in self.headers: - self.add_header('Last-Modified', time_to_http(getctime(path))) + if "Last-Modified" not in self.headers: + self.add_header("Last-Modified", time_to_http(getctime(path))) class GeneratorResponse(BaseResponse): - """For response, which use generator as returned value. + """For a response that uses a generator as the returned value. - Even though you can figure out iterating your generator more times, just - like Response, instance of GeneratorResponse can be used only once! + Even though you can figure out how to iterate your generator + multiple times, just like a Response, an instance of a + GeneratorResponse can be used only once! """ - def __init__(self, generator: Iterable[bytes], - content_type: str = "text/html; charset=utf-8", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK, - content_length: int = 0): - super().__init__(content_type=content_type, - headers=headers, - status_code=status_code) + def __init__( + self, + generator: Iterable[bytes], + content_type: str = "text/html; charset=utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + content_length: int = 0, + ): + super().__init__( + content_type=content_type, headers=headers, status_code=status_code + ) self._content_length = content_length self.__generator = generator @@ -585,15 +652,15 @@ def __range_generator__(self): start = 0 if pos < self._start: start = self._start - pos - if self._end and (pos+length) > self._end: + if self._end and (pos + length) > self._end: end = (self._end + 1) - pos pos += length yield data[start:end] # is enough if self._end and pos > self._end: - return b'' - return b'' + return b"" + return b"" def __end_of_response__(self): if self._start is not None or self._end is not None: @@ -602,14 +669,21 @@ def __end_of_response__(self): class StrGeneratorResponse(GeneratorResponse): - """Generator response where generator returns str.""" - - def __init__(self, generator: Iterable[str], - content_type: str = "text/html; charset=utf-8", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK): - super().__init__([b''], content_type=content_type, headers=headers, - status_code=status_code) + """A generator response where the generator returns a string.""" + + def __init__( + self, + generator: Iterable[str], + content_type: str = "text/html; charset=utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + ): + super().__init__( + [b""], + content_type=content_type, + headers=headers, + status_code=status_code, + ) self.__generator: Iterable[str] = generator def __end_of_response__(self): @@ -617,78 +691,92 @@ def __end_of_response__(self): class JSONGeneratorResponse(StrGeneratorResponse): - """JSON Response for data from generator. + """A JSON Response for data from a generator. - Data will be processed in generator way, so they need to be buffered. - This class need simplejson module. + The data will be processed in a generator fashion, so it needs to be + buffered. + This class requires the simplejson module. - ** kwargs from constructor are serialized to json structure. + The ``**kwargs`` from the constructor are serialized to a JSON structure. """ - def __init__(self, charset: str = "utf-8", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK, - **kwargs): + + def __init__( + self, + charset: str = "utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, + **kwargs, + ): if not JSON_GENERATOR: # pyl-int: disable=super-init-not-called raise NotImplementedError( - "JSONGeneratorResponse need simplejson module") + "JSONGeneratorResponse need simplejson module" + ) mime_type = "application/json" if charset: - mime_type += "; charset="+charset + mime_type += "; charset=" + charset generator = JSONEncoder( # type: ignore - iterable_as_array=True).iterencode(kwargs) # type: ignore + iterable_as_array=True + ).iterencode(kwargs) # type: ignore super().__init__(generator, mime_type, headers, status_code) class NoContentResponse(BaseResponse): - """For situation, where only state is returned.""" + """For situations where only a status is returned.""" - def __init__(self, - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_NO_CONTENT): + def __init__( + self, + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_NO_CONTENT, + ): super().__init__(headers=headers, status_code=status_code) def __start_response__(self, start_response: Callable): - start_response( - "%d %s" % (self.status_code, self.reason), []) + start_response("%d %s" % (self.status_code, self.reason), []) class EmptyResponse(NoContentResponse): - """Compatibility response""" + """Compatibility response.""" + @deprecated("use NoContentResponse instead.") def __init__(self, status_code: int = HTTP_NO_CONTENT): super().__init__(status_code=status_code) class Declined(NoContentResponse): - """For situation without answer. + """For situations without an answer. - This response is returned, when state.DECLINED was returned. + This response is returned when state.DECLINED is returned. """ + def __init__(self, status_code: int = HTTP_OK): super().__init__(status_code=status_code) @property def headers(self): - """Declined response don't have headers.""" + """A Declined response does not have headers.""" return Headers() @headers.setter def headers(self, value): # pylint: disable=unused-argument,logging-format-interpolation stack_record = stack()[1] - log.warning("Declined response don't use headers.\n" - " File {1}, line {2}, in {3} \n" - "{0}".format(stack_record[4][0], *stack_record[1:4])) + log.warning( + "Declined response don't use headers.\n" + " File {1}, line {2}, in {3} \n" + "{0}".format(stack_record[4][0], *stack_record[1:4]) + ) def add_header(self, *args, **kwargs): - """Declined response don't have headers""" + """A Declined response does not have headers.""" # pylint: disable=unused-argument,logging-format-interpolation stack_record = stack()[1] - log.warning("Declined response don't use headers.\n" - " File {1}, line {2}, in {3} \n" - "{0}".format(stack_record[4][0], *stack_record[1:4])) + log.warning( + "Declined response don't use headers.\n" + " File {1}, line {2}, in {3} \n" + "{0}".format(stack_record[4][0], *stack_record[1:4]) + ) def __call__(self, start_response: Callable): log.debug("DECLINED") @@ -696,65 +784,73 @@ def __call__(self, start_response: Callable): class RedirectResponse(Response): - """Redirect the browser to another location. + """Redirects the browser to another location. - A short text is sent to the browser informing that the document has moved - (for those rare browsers that do not support redirection); this text can - be overridden by supplying a text string (``message``). + A short text is sent to the browser informing it that the document + has moved (for those rare browsers that do not support redirection); + this text can be overridden by supplying a text string (``message``). - When ``permanent`` or ``status_code`` is true, MOVED_PERMANENTLY - status code will be sent to the client, otherwise it will be - MOVED_TEMPORARILY. **Argument ``permanent`` and ``status_code`` as boolean - is deprecated. Use real status_code instead.** + When ``permanent`` is True or ``status_code`` is set to a redirect + status, the corresponding status code will be sent to the client. + The use of ``permanent`` and boolean values for ``status_code`` is + deprecated; use the actual status_code instead. """ - def __init__(self, location: str, - status_code: Union[int, bool] = HTTP_MOVED_TEMPORARILY, - message: Union[str, bytes] = b'', - headers: Optional[Union[Headers, HeadersList]] = None, - permanent: bool = False): + def __init__( + self, + location: str, + status_code: Union[int, bool] = HTTP_MOVED_TEMPORARILY, + message: Union[str, bytes] = b"", + headers: Optional[Union[Headers, HeadersList]] = None, + permanent: bool = False, + ): if status_code is True or permanent: - log.warning('Argument `permanent` is deprecated. ' - ' Use real status_code instead.') + log.warning( + "Argument `permanent` is deprecated. " + " Use real status_code instead." + ) status_code = HTTP_MOVED_PERMANENTLY - super().__init__(message, - content_type="text/plain", - headers=headers, - status_code=status_code) + super().__init__( + message, + content_type="text/plain", + headers=headers, + status_code=status_code, + ) self.add_header("Location", location) class NotModifiedResponse(NoContentResponse): - """Not Modified Response.""" - - def __init__(self, - headers: Optional[Union[Headers, HeadersList]] = None, - etag: Optional[str] = None, - content_location: Optional[str] = None, - date: Optional[Union[str, int, datetime]] = None, - vary: Optional[str] = None): - + """A Not Modified Response.""" + + def __init__( + self, + headers: Optional[Union[Headers, HeadersList]] = None, + etag: Optional[str] = None, + content_location: Optional[str] = None, + date: Optional[Union[str, int, datetime]] = None, + vary: Optional[str] = None, + ): super().__init__(status_code=HTTP_NOT_MODIFIED, headers=headers) if etag: - self.add_header('ETag', etag) + self.add_header("ETag", etag) if content_location: - self.add_header('Content-Location', content_location) + self.add_header("Content-Location", content_location) if isinstance(date, str) and date: - self.add_header('Date', date) + self.add_header("Date", date) elif isinstance(date, int): - self.add_header('Date', time_to_http(date)) + self.add_header("Date", time_to_http(date)) elif isinstance(date, datetime): - self.add_header('Date', datetime_to_http(date)) + self.add_header("Date", datetime_to_http(date)) if vary: - self.add_header('Vary', vary) + self.add_header("Vary", vary) class ResponseError(RuntimeError): - """Exception for bad response values.""" + """An exception for bad response values.""" class HTTPException(Exception): - """HTTP Exception to fast stop work. + """An HTTP Exception to quickly stop execution. Simple error exception: @@ -774,54 +870,58 @@ class HTTPException(Exception): """ def __init__(self, arg: Union[int, BaseResponse], **kwargs): - """status_code is one of HTTP_* status code from state module. + """The status_code is one of the HTTP_* status codes from the state + module. - If response is set, that will use, otherwise the handler from - Application will be call.""" + If a response is set, it will be used; otherwise, the handler from + the Application will be called.""" assert isinstance(arg, (int, BaseResponse)) super().__init__(arg, kwargs) def make_response(self): - """Return or make a response if is possible.""" + """Returns or creates a response if possible.""" if isinstance(self.args[0], BaseResponse): return self.args[0] status_code = self.args[0] if status_code == DECLINED: - return Declined() # decline the connection + return Declined() # decline the connection if status_code == HTTP_OK: return EmptyResponse() return None @property def response(self): - """Return response if it was set.""" + """Returns the response if it was set.""" if isinstance(self.args[0], BaseResponse): return self.args[0] return None @property def status_code(self): - """Return status code from exception or Response.""" + """Returns the status code from the exception or Response.""" if isinstance(self.args[0], int): return self.args[0] return self.args[0].status_code -def make_response(data: Optional[Union[str, bytes, dict, Iterable[bytes]]], - content_type: str = "text/html; charset=utf-8", - headers: Optional[Union[Headers, HeadersList]] = None, - status_code: int = HTTP_OK): - """Create response from values. +def make_response( + data: Optional[Union[str, bytes, dict, Iterable[bytes]]], + content_type: str = "text/html; charset=utf-8", + headers: Optional[Union[Headers, HeadersList]] = None, + status_code: int = HTTP_OK, +): + """Creates a response from values. - If data are: + If data is: - :str, bytes: Response is returned. - :list, dict: JSONResponse is returned. List can't be list of bytes, - otherwise GeneratorResponse is returned. - :Iterable: GeneratorResponse is returned + :str, bytes: A Response is returned. + :list, dict: A JSONResponse is returned. A list cannot be a list + of bytes; otherwise, a GeneratorResponse is returned. + :Iterable: A GeneratorResponse is returned. - Data could be string, bytes, or bytes returns iterable object like file. + Data can be a string, bytes, or a bytes-returning iterable object + like a file. Response from string: @@ -882,7 +982,7 @@ def make_response(data: Optional[Union[str, bytes, dict, Iterable[bytes]]], [b'key', b'value'] NoContentResponse from None, content_type argument is ignored. If - status_code is leave 200 OK, then status will be 204 No Content: + status_code is left at 200 OK, then status will be 204 No Content: >>> res = make_response(None) >>> res @@ -896,7 +996,7 @@ def make_response(data: Optional[Union[str, bytes, dict, Iterable[bytes]]], """ try: - if isinstance(data, (str, bytes)): # "hello world" + if isinstance(data, (str, bytes)): # "hello world" return Response(data, content_type, headers, status_code) if isinstance(data, dict): return JSONResponse(data, headers=headers, status_code=status_code) @@ -910,42 +1010,50 @@ def make_response(data: Optional[Union[str, bytes, dict, Iterable[bytes]]], iter(data) # try iter data return GeneratorResponse(data, content_type, headers, status_code) except Exception: # pylint: disable=broad-except - log.exception("Error in processing values: %s, %s, %s, %s", - type(data), type(content_type), type(headers), - type(status_code)) + log.exception( + "Error in processing values: %s, %s, %s, %s", + type(data), + type(content_type), + type(headers), + type(status_code), + ) raise ResponseError( "Returned data must by: ," - " , , ") + " , , " + ) -def redirect(location: str, - status_code: Union[int, bool] = HTTP_MOVED_TEMPORARILY, - message: Union[str, bytes] = b'', - headers: Optional[Union[Headers, HeadersList]] = None, - permanent: bool = False): - """Raise HTTPException with RedirectResponse response. +def redirect( + location: str, + status_code: Union[int, bool] = HTTP_MOVED_TEMPORARILY, + message: Union[str, bytes] = b"", + headers: Optional[Union[Headers, HeadersList]] = None, + permanent: bool = False, +): + """Raises an HTTPException with a RedirectResponse. - See RedirectResponse, with same interface for more information about - response. + See RedirectResponse for more information about the response, as it has the + same interface. """ raise HTTPException( - RedirectResponse(location, status_code, message, headers, permanent)) + RedirectResponse(location, status_code, message, headers, permanent) + ) def abort(arg: Union[int, BaseResponse]): - """Raise HTTPException with arg. + """Raises an HTTPException with the given argument. - Raise simple error exception: + Raises a simple error exception: >>> abort(404) Traceback (most recent call last): ... poorwsgi.response.HTTPException: (404, {}) - Raise exception with response: + Raises an exception with a response: - >>> abort(Response(data=b'Created', status_code=201)) + >>> abort(Response(data=b'Created', status_code=201)) # doctest: +ELLIPSIS Traceback (most recent call last): ... poorwsgi.response.HTTPException: diff --git a/poorwsgi/results.py b/poorwsgi/results.py index be6da52..ce41210 100644 --- a/poorwsgi/results.py +++ b/poorwsgi/results.py @@ -1,37 +1,48 @@ -"""Default Poor WSGI handlers. +"""Default PoorWSGI handlers. :Functions: not_modified, internal_server_error, bad_request, forbidden, not_found, method_not_allowed, not_implemented, directory_index, debug_info """ -from traceback import format_exception -from time import strftime, gmtime -from os.path import isfile, isdir, getsize, getctime -from operator import itemgetter -from sys import version, exc_info +import mimetypes +import os +from hashlib import sha256 from inspect import cleandoc from logging import getLogger -from hashlib import sha256 -from typing import Dict, Callable, Optional - -import os -import mimetypes +from operator import itemgetter +from os.path import getctime, getsize, isdir, isfile +from sys import exc_info, version +from time import gmtime, strftime +from traceback import format_exception +from typing import Callable, Dict, Optional -from poorwsgi.response import Response, NotModifiedResponse, HTTPException -from poorwsgi.state import METHOD_ALL, methods, sorted_methods, \ - HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, \ - HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_INTERNAL_SERVER_ERROR, \ - HTTP_NOT_IMPLEMENTED, \ - __version__, __date__ -from poorwsgi.session import get_token from poorwsgi.headers import time_to_http - -HTML_ESCAPE_TABLE = {'&': "&", - '"': """, - "'": "'", - '>': ">", - '<': "<"} +from poorwsgi.response import HTTPException, NotModifiedResponse, Response +from poorwsgi.session import get_token +from poorwsgi.state import ( + HTTP_BAD_REQUEST, + HTTP_FORBIDDEN, + HTTP_INTERNAL_SERVER_ERROR, + HTTP_METHOD_NOT_ALLOWED, + HTTP_NOT_FOUND, + HTTP_NOT_IMPLEMENTED, + HTTP_NOT_MODIFIED, + HTTP_UNAUTHORIZED, + METHOD_ALL, + __date__, + __version__, + methods, + sorted_methods, +) + +HTML_ESCAPE_TABLE = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", +} log = getLogger("poorwsgi") @@ -43,19 +54,19 @@ def html_escape(text: str): - """Escape to html entities.""" - return ''.join(HTML_ESCAPE_TABLE.get(c, c) for c in text) + """Escapes a string for HTML entities.""" + return "".join(HTML_ESCAPE_TABLE.get(c, c) for c in text) def hbytes(val: float): - """Return pair value and unit. + """Returns a value and its unit as a pair. >>> hbytes(2000000) (1.9..., 'M') >>> hbytes(1024.0) (1.0, 'k') """ - unit = ('', 'k', 'M', 'G', 'T', 'P') + unit = ("", "k", "M", "G", "T", "P") u = 0 while val > 1000 and u < len(unit): u += 1 @@ -64,14 +75,14 @@ def hbytes(val: float): def human_methods_(m): - """Return methods in text.""" + """Returns methods as a text string.""" if m == METHOD_ALL: - return 'ALL' - return ' | '.join(key for key, val in sorted_methods if val & m) + return "ALL" + return " | ".join(key for key, val in sorted_methods if val & m) def handlers_view(handlers, sort=True): - """Returns sorted handlers list.""" + """Returns a sorted list of handlers.""" rv = [] for u, d in sorted(handlers.items()) if sort else handlers.items(): vt = {} @@ -86,33 +97,40 @@ def handlers_view(handlers, sort=True): def not_modified(req): - """Return NotModifiedResponse. + """Returns a NotModifiedResponse. - Headers ETag, Content-Location is return from request. - Date header will be set. + The ETag and Content-Location headers are returned from the request. + The Date header will be set. """ return NotModifiedResponse( - etag=req.headers.get('ETag'), - content_location=req.headers.get('Content-Location'), - date=time_to_http()) + etag=req.headers.get("ETag"), + content_location=req.headers.get("Content-Location"), + date=time_to_http(), + ) def internal_server_error(req, *_): - """ More debug 500 Internal Server Error server handler. + """A more debug-friendly 500 Internal Server Error server handler. - It was be called automatically when no handlers are not defined - in dispatch_table.errors. If poor_Debug variable is to On, Tracaback - will be generated. + It is called automatically when no handlers are defined in + dispatch_table.errors. If the poor_Debug variable is set to On, a + traceback will be generated. """ handler = {"module": None, "name": None, "args": None} if req.uri_handler: handler["module"] = req.uri_handler.__module__ handler["name"] = req.uri_handler.__name__ - handler["args"] = ', '.join(req.uri_handler.__code__.co_varnames) - - log.exception("Handler `%s.%s(%s)' for %s [%s]", - handler["module"], handler["name"], handler["args"], - req.uri, req.uri_rule, exc_info=True) + handler["args"] = ", ".join(req.uri_handler.__code__.co_varnames) + + log.exception( + "Handler `%s.%s(%s)' for %s [%s]", + handler["module"], + handler["name"], + handler["args"], + req.uri, + req.uri_rule, + exc_info=True, + ) exc_type, exc_value, exc_traceback = exc_info() @@ -132,7 +150,8 @@ def internal_server_error(req, *_): " \n" " \n" " \n" - "

                500 - Internal Server Error

                \n") + "

                500 - Internal Server Error

                \n" + ) if req.debug: uri = html_escape(req.uri) @@ -146,45 +165,41 @@ def internal_server_error(req, *_): f" uri_rule: {uri_rule}
                \n" " uri_handler: " f"{handler['module']}.{handler['name']}({handler['args']})" - "
                \n") + "
                \n" + ) - res.write( - "

                Exception Traceback

                \n" - "
                \n")
                +        res.write("  

                Exception Traceback

                \n
                \n")
                 
                         # Traceback
                -        traceback = format_exception(exc_type,
                -                                     exc_value,
                -                                     exc_traceback)
                -        traceback = ''.join(traceback)
                -        traceback = traceback.split('\n')
                +        traceback = format_exception(exc_type, exc_value, exc_traceback)
                +        traceback = "".join(traceback)
                +        traceback = traceback.split("\n")
                 
                         for i, line in enumerate(traceback):
                             traceback_line = html_escape(line)
                -            res.write('%s\n' %
                -                      (i % 2, traceback_line))
                +            res.write(
                +                '%s\n' % (i % 2, traceback_line)
                +            )
                 
                         res.write(
                             "  
                \n" "
                \n" " %s / Poor WSGI for Python ,webmaster: %s" - "\n" % (req.server_software, req.server_admin)) + "\n" % (req.server_software, req.server_admin) + ) else: res.write( "
                \n" - " webmaster: %s \n" % req.server_admin) - # endif + " webmaster: %s \n" % req.server_admin + ) - res.write( - " \n" - "") + res.write(" \n") return res -# enddef def bad_request(req, error=None): - """ 400 Bad Request server error handler. """ + """A 400 Bad Request server error handler.""" if error: log.warning("400 - Bad Request: %s", error) path = "[ NOT PARSED ]" @@ -214,39 +229,47 @@ def bad_request(req, error=None): "
                \n" " webmaster: %s \n" " \n" - "" % (req.method, html_escape(path), error or "", - req.server_admin)) + "" + % (req.method, html_escape(path), error or "", req.server_admin) + ) return Response(content, status_code=HTTP_BAD_REQUEST) -def unauthorized(req, realm=None, stale='', error=None): - """Return 401 Unauthorized response.""" +def unauthorized(req, realm=None, stale="", error=None): + """Returns a 401 Unauthorized response.""" if error: log.warning("401 - Unauthorized response: %s", error) headers = None - if req.app.auth_type == 'Digest': + if req.app.auth_type == "Digest": if not realm: raise RuntimeError("Digest: realm value must be set") - nonce = get_token(req.secret_key, req.user_agent, - timeout=req.app.auth_timeout) + nonce = get_token( + req.secret_key, req.user_agent, timeout=req.app.auth_timeout + ) opaque = sha256(req.server_hostname.encode()).hexdigest() - qop = req.app.auth_qop or '' + qop = req.app.auth_qop or "" if qop: qop = 'qop="%s",' % req.app.auth_qop header = ( 'Digest realm="{realm}",{qop}algorithm="{algorithm}",' 'nonce="{nonce}",opaque="{opaque}"' - ''.format(realm=realm, qop=qop, algorithm=req.app.auth_algorithm, - nonce=nonce, opaque=opaque)) + "".format( + realm=realm, + qop=qop, + algorithm=req.app.auth_algorithm, + nonce=nonce, + opaque=opaque, + ) + ) if stale: - header += ',stale=true' + header += ",stale=true" # Headers could be tuple, than each header value must be another # available authenticate method, for example SHA-256 algorithm. - headers = {'WWW-Authenticate': header} + headers = {"WWW-Authenticate": header} content = ( "\n" @@ -267,13 +290,14 @@ def unauthorized(req, realm=None, stale='', error=None): "
                \n" " webmaster: %s \n" " \n" - "" % (req.method, html_escape(req.uri), req.server_admin)) + "" % (req.method, html_escape(req.uri), req.server_admin) + ) return Response(content, headers=headers, status_code=HTTP_UNAUTHORIZED) def forbidden(req, error=None): - """ 403 - Forbidden Access server error handler. """ + """A 403 - Forbidden Access server error handler.""" if error: log.warning("400 - Forbidden Access: %s", error) @@ -297,13 +321,13 @@ def forbidden(req, error=None): "
                \n" " webmaster: %s \n" " \n" - "" % (html_escape(req.uri), req.server_admin)) + "" % (html_escape(req.uri), req.server_admin) + ) return Response(content, status_code=HTTP_FORBIDDEN) -# enddef def not_found(req, error=None): - """ 404 - Page Not Found server error handler. """ + """A 404 - Page Not Found server error handler.""" if error: log.warning("404 - Not Found: %s", error) @@ -326,13 +350,13 @@ def not_found(req, error=None): "
                \n" " webmaster: %s \n" " \n" - "" % (html_escape(req.uri), req.server_admin)) + "" % (html_escape(req.uri), req.server_admin) + ) return Response(content, status_code=HTTP_NOT_FOUND) -# enddef def method_not_allowed(req, error=None): - """ 405 Method Not Allowed server error handler. """ + """A 405 Method Not Allowed server error handler.""" if error: log.warning("405 - Method Not Allowed: %s", error) @@ -356,13 +380,13 @@ def method_not_allowed(req, error=None): "
                \n" " webmaster: %s \n" " \n" - "" % (req.method, html_escape(req.uri), req.server_admin)) + "" % (req.method, html_escape(req.uri), req.server_admin) + ) return Response(content, status_code=HTTP_METHOD_NOT_ALLOWED) -# enddef def not_implemented(req, code: Optional[int] = None, error=None): - """ 501 Not Implemented server error handler. """ + """A 501 Not Implemented server error handler.""" if error: log.error("501 - Not Implemented: %s", error) @@ -380,38 +404,44 @@ def not_implemented(req, code: Optional[int] = None, error=None): " \n" " \n" " \n" - "

                501 - Not Implemented

                \n") + "

                501 - Not Implemented

                \n" + ) if code: content += ( "

                Your reqeuest %s returned not implemented\n" - " status code %s.

                \n" % (html_escape(req.uri), - code)) - log.error('Your reqeuest %s returned not implemented status code %s', - html_escape(req.uri), code) + " status code %s.

                \n" + % (html_escape(req.uri), code) + ) + log.error( + "Your reqeuest %s returned not implemented status code %s", + html_escape(req.uri), + code, + ) else: content += ( "

                Response for Your reqeuest %s\n" - " is not implemented

                " % html_escape(req.uri)) - # endif + " is not implemented

                " % html_escape(req.uri) + ) content += ( "
                \n" " webmaster: %s \n" " \n" - "" % req.server_admin) + "" % req.server_admin + ) return Response(content, status_code=HTTP_NOT_IMPLEMENTED) -# enddef def directory_index(req, path): # noqa: C901 - """Returns directory index as html page.""" + """Returns a directory index as an HTML page.""" if not isdir(path): log.error( "Only directory_index can be send with directory_index handler. " "`%s' is not directory.", - path) + path, + ) raise HTTPException(HTTP_INTERNAL_SERVER_ERROR) last_modified = time_to_http(getctime(path)) @@ -422,7 +452,7 @@ def directory_index(req, path): # noqa: C901 index.sort() - diruri = html_escape(req.uri.rstrip('/')) + diruri = html_escape(req.uri.rstrip("/")) content = ( "\n" "\n" @@ -442,7 +472,8 @@ def directory_index(req, path): # noqa: C901 "
                \n" " \n" " " - "\n" % (diruri, diruri)) + '\n' % (diruri, diruri) + ) for item in index: # dot files @@ -456,94 +487,117 @@ def directory_index(req, path): # noqa: C901 if not os.access(fpath, os.R_OK): continue - fname = item + ('/' if isdir(fpath) else '') + fname = item + ("/" if isdir(fpath) else "") ftype = "" if isfile(fpath): # pylint: disable=unused-variable - (ftype, encoding) = mimetypes.guess_type(fpath) + (ftype, _) = mimetypes.guess_type(fpath) if not ftype: - ftype = 'application/octet-stream' + ftype = "application/octet-stream" size = "%.1f%s" % hbytes(getsize(fpath)) elif isdir(fpath): ftype = "Directory" size = "-" else: - size = ftype = '-' + size = ftype = "-" content += ( - " " - "\n" % - (diruri + '/' + fname, - fname, - strftime("%d-%b-%Y %H:%M", gmtime(getctime(fpath))), - size, - ftype)) + ' ' + '\n' + % ( + diruri + "/" + fname, + fname, + strftime("%d-%b-%Y %H:%M", gmtime(getctime(fpath))), + size, + ftype, + ) + ) - content += ( - "
                NameLast ModifiedSizeType
                SizeType
                %s%s%s%s
                %s%s%s%s
                \n" - "
                \n") + content += " \n
                \n" if req.debug: content += ( " %s / Poor WSGI for Python, " - "webmaster: %s \n" % - (req.server_software, req.server_admin) + "webmaster: %s \n" + % (req.server_software, req.server_admin) ) else: - content += (" webmaster: %s \n" % - req.server_admin) + content += ( + " webmaster: %s \n" % req.server_admin + ) - content += ( - " \n" - "") + content += " \n" - return (content, "text/html; character=utf-8", - ('Last-Modified', last_modified)) + return ( + content, + "text/html; character=utf-8", + ("Last-Modified", last_modified), + ) def debug_info(req, app): - """Return debug page. + """Returns the debug page. - When Application.debug is enable, this handler is used for /debug-info. + When Application.debug is enabled, this handler is used for /debug-info. """ # pylint: disable=too-many-locals # transform static handlers table to html shandlers_html = "Static:\n" shandlers_html += "\n".join( - (' %s' - '%s%s' % - (html_escape(u), html_escape(u), human_methods_(m), - f.__module__+'.'+f.__name__) - for u, m, f in handlers_view(app.routes))) + ( + ' %s' + "%s%s" + % ( + html_escape(u), + html_escape(u), + human_methods_(m), + f.__module__ + "." + f.__name__, + ) + for u, m, f in handlers_view(app.routes) + ) + ) # regular expression handlers rhandlers_html = "Regular expression:\n" rhandlers_html += "\n".join( - ('
                %s
                ' - '%s%s%s' % - (html_escape(r or u.pattern), - ', '.join(tuple("%s:%s" % (G, C.__name__) for G, C in c)), - human_methods_(m), - f.__module__+'.'+f.__name__) - for u, m, (f, c, r) in handlers_view(app.regular_routes, False))) + ( + '
                %s
                ' + "%s%s%s" + % ( + html_escape(r or u.pattern), + ", ".join( + tuple("%s:%s" % (G, C.__name__) for G, C in c) + ), + human_methods_(m), + f.__module__ + "." + f.__name__, + ) + for u, m, (f, c, r) in handlers_view(app.regular_routes, False) + ) + ) dhandlers_html = "Default:\n" # this function could be called by user, so we need to test req.debug - if req.debug and 'debug-info' not in app.routes: - dhandlers_html += (' %s' - '%s%s\n' % - ('/debug-info', - '/debug-info', - 'ALL', - debug_info.__module__+'.'+debug_info.__name__)) + if req.debug and "debug-info" not in app.routes: + dhandlers_html += ( + ' %s' + "%s%s\n" + % ( + "/debug-info", + "/debug-info", + "ALL", + debug_info.__module__ + "." + debug_info.__name__, + ) + ) dhandlers_html += "\n".join( - (' _default_handler_' - '%s%s' % - (human_methods_(m), - f.__module__+'.'+f.__name__) - for x, m, f in handlers_view({'x': app.defaults}))) + ( + ' _default_handler_' + "%s%s" + % (human_methods_(m), f.__module__ + "." + f.__name__) + for x, m, f in handlers_view({"x": app.defaults}) + ) + ) # transform state handlers and default state table to html, users handler # from shandlers are preferer @@ -556,73 +610,95 @@ def debug_info(req, app): _tmp_shandlers[key] = val ehandlers_html = "\n".join( - " %s%s%s" % - (c, human_methods_(m), f.__module__+'.'+f.__name__) - for c, m, f in handlers_view(_tmp_shandlers)) + " %s%s%s" + % (c, human_methods_(m), f.__module__ + "." + f.__name__) + for c, m, f in handlers_view(_tmp_shandlers) + ) # pre and post table pre, post = app.before, app.after if len(pre) >= len(post): - post += (len(pre)-len(post)) * (None, ) + post += (len(pre) - len(post)) * (None,) else: - pre += (len(post)-len(pre)) * (None, ) + pre += (len(post) - len(pre)) * (None,) pre_post_html = "\n".join( - " %s%s" % - (f0.__module__+'.'+f0.__name__ if f0 is not None else '', - f1.__module__+'.'+f1.__name__ if f1 is not None else '',) - for f0, f1 in zip(pre, post)) + " %s%s" + % ( + f0.__module__ + "." + f0.__name__ if f0 is not None else "", + f1.__module__ + "." + f1.__name__ if f1 is not None else "", + ) + for f0, f1 in zip(pre, post) + ) # filters filters_html = "\n".join( - " %s%s%s" % - (f, html_escape(str(r)), c.__name__) - for f, (r, c) in app.filters.items()) + " %s%s%s" + % (f, html_escape(str(r)), c.__name__) + for f, (r, c) in app.filters.items() + ) # transform actual request headers to hml - headers_html = "\n".join(( - " %s:%s" % - (key, html_escape(val)) for key, val in req.headers.items())) + headers_html = "\n".join( + ( + " %s:%s" % (key, html_escape(val)) + for key, val in req.headers.items() + ) + ) # transform some poor wsgi variables to html - poor_html = "\n".join(( - " %s:%s" % - (key, html_escape(str(val))) for key, val in ( - ('Debug', req.debug), - ('Version', "%s (%s)" % (__version__, __date__)), - ('Python Version', version), - ('Server Software', req.server_software), - ('Server Hostname', req.server_hostname), - ('Server Port', req.server_port), - ('Server Scheme', req.server_scheme), - ('HTTP Hostname', req.hostname), - ('Server Admin', req.server_admin), - ('Forward For', req.forwarded_for), - ('Forward Host', req.forwarded_host), - ('Forward Proto', req.forwarded_proto), - ('Document Root', req.document_root), - ('Document Index', req.document_index), - ('Secret Key', '*'*5 + ' see in error output (wsgi log)' - ' when Log Level is debug ' + '*'*5) - ))) - log.debug('SecretKey: %s', repr(req.secret_key)) + poor_html = "\n".join( + ( + " %s:%s" + % (key, html_escape(str(val))) + for key, val in ( + ("Debug", req.debug), + ("Version", "%s (%s)" % (__version__, __date__)), + ("Python Version", version), + ("Server Software", req.server_software), + ("Server Hostname", req.server_hostname), + ("Server Port", req.server_port), + ("Server Scheme", req.server_scheme), + ("HTTP Hostname", req.hostname), + ("Server Admin", req.server_admin), + ("Forward For", req.forwarded_for), + ("Forward Host", req.forwarded_host), + ("Forward Proto", req.forwarded_proto), + ("Document Root", req.document_root), + ("Document Index", req.document_index), + ( + "Secret Key", + "*" * 5 + " see in error output (wsgi log)" + " when Log Level is debug " + "*" * 5, + ), + ) + ) + ) + log.debug("SecretKey: %s", repr(req.secret_key)) # tranform application variables to html - app_html = "\n".join(( - " %s:%s" % - (key, html_escape(val)) for key, val in req.get_options().items())) + app_html = "\n".join( + ( + " %s:%s" % (key, html_escape(val)) + for key, val in req.get_options().items() + ) + ) environ = req.environ.copy() - if hasattr(os, 'getgid'): - environ['os.pgid'] = os.getgid() - environ['os.puid'] = os.getuid() - environ['os.egid'] = os.getegid() - environ['os.euid'] = os.geteuid() + if hasattr(os, "getgid"): + environ["os.pgid"] = os.getgid() + environ["os.puid"] = os.getuid() + environ["os.egid"] = os.getegid() + environ["os.euid"] = os.geteuid() # transfotm enviroment variables to html - environ_html = "\n".join(( - " %s:%s" % - (key, html_escape(str(val))) for key, val in sorted(environ.items()))) + environ_html = "\n".join( + ( + " %s:%s" + % (key, html_escape(str(val))) + for key, val in sorted(environ.items()) + ) + ) content_html = cleandoc( """ @@ -709,21 +785,23 @@ def debug_info(req, app):
                %s / Poor WSGI for Python , webmaster: %s - """) % (shandlers_html, - rhandlers_html, - dhandlers_html, - ehandlers_html, - pre_post_html, - filters_html, - headers_html, - poor_html, - app_html, - environ_html, - req.server_software, - req.server_admin) + """ + ) % ( + shandlers_html, + rhandlers_html, + dhandlers_html, + ehandlers_html, + pre_post_html, + filters_html, + headers_html, + poor_html, + app_html, + environ_html, + req.server_software, + req.server_admin, + ) return content_html -# enddef def __fill_default_shandlers(code: int, handler: Callable): @@ -741,6 +819,14 @@ def __fill_default_shandlers(code: int, handler: Callable): __fill_default_shandlers(HTTP_INTERNAL_SERVER_ERROR, internal_server_error) __fill_default_shandlers(HTTP_NOT_IMPLEMENTED, not_implemented) -__all__ = ['not_modified', 'internal_server_error', 'bad_request', 'forbidden', - 'not_found', 'method_not_allowed', 'not_implemented', - 'directory_index', 'debug_info'] +__all__ = [ + "bad_request", + "debug_info", + "directory_index", + "forbidden", + "internal_server_error", + "method_not_allowed", + "not_found", + "not_implemented", + "not_modified", +] diff --git a/poorwsgi/session.py b/poorwsgi/session.py index 868e1a7..0db26cd 100644 --- a/poorwsgi/session.py +++ b/poorwsgi/session.py @@ -25,13 +25,12 @@ def hidden(text: Union[str, bytes], passwd: Union[str, bytes]) -> bytes: - """(en|de)crypt text with sha hash of passwd via xor. + """(En|de)crypts text with a SHA hash of the password via XOR. - Arguments: - text : str or bytes - raw data to (en|de)crypt - passwd : str or bytes - password + text + Raw data to (en|de)crypt. + passwd + The password. """ if isinstance(passwd, bytes): passwd = sha512(passwd).digest() @@ -57,10 +56,10 @@ def hidden(text: Union[str, bytes], passwd: Union[str, bytes]) -> bytes: def get_token(secret: str, client: str, timeout: Optional[int] = None, expired: int = 0): - """Create token from secret, and client string. + """Creates a token from a secret and client string. - If timeout is set, token contains time align with twice of this value. - Twice, because time of creating can be so near to computed time. + If timeout is set, the token contains a time aligned with twice this value. + This is because the creation time can be very close to the computed time. """ if timeout is None: text = "%s%s" % (secret, client) @@ -75,10 +74,11 @@ def get_token(secret: str, client: str, timeout: Optional[int] = None, def check_token(token: str, secret: str, client: str, timeout: Optional[int] = None): - """Check token, if it is right. + """Checks if the token is correct. - Arguments secret, client and expired must be same, when token was - generated. If expired is set, than token must be younger than 2*expired. + The secret, client, and timeout arguments must match those used when + the token was generated. If timeout is set, the token must not be + older than 2 * timeout. """ if timeout is None: return token == get_token(secret, client) @@ -95,67 +95,66 @@ def check_token(token: str, secret: str, client: str, class SessionError(RuntimeError): - """Base Exception for Session""" + """Base Exception for Session.""" class NoCompress: - """Fake compress class/module whith two static method for PoorSession. + """Fake compress class/module with two static methods for PoorSession. - If compress parameter is None, this class is use. + If the compress parameter is None, this class is used. """ - @staticmethod def compress(data, compresslevel=0): # pylint: disable=unused-argument - """Get two params, data, and compresslevel. Method only return data.""" + """Accepts data and compresslevel, and returns data unchanged.""" return data @staticmethod def decompress(data): - """Get one parameter data, which returns.""" + """Accepts data and returns it unchanged.""" return data class PoorSession: - """Self-contained cookie with session data. + """A self-contained cookie with session data. - You cat store or read data from object via PoorSession.data variable which - must be dictionary. Data is stored to cookie by pickle dump, and next - hidden with app.secret_key. So it must be set on Application object or with - poor_SecretKey environment variable. Be careful with stored object. You can - add object with little python trick: + You can store or read data from the object via the PoorSession.data + variable, which must be a dictionary. Data is stored to the cookie by + JSON serialization and then hidden with app.secret_key. Therefore, it + must be set on the Application object or with the poor_SecretKey + environment variable. Be careful with stored objects. You can add + objects with a little Python trick: .. code:: python sess = PoorSession(app.secret_key) - sess.data['class'] = obj.__class__ # write to cookie + sess.data['class'] = obj.__class__.__name__ # write to cookie sess.data['dict'] = obj.__dict__.copy() - obj = sess.data['class']() # read from cookie + obj = globals()[sess.data['class']]() # read from cookie obj.__dict__ = sess.data['dict'].copy() - Or for beter solution, you can create export and import methods for you - object like that: + For a better solution, you can create export and import methods for + your object like this: .. code:: python - class Obj(object): - def import(self, d): + class Obj: + def from_dict(self, d): self.attr1 = d['attr1'] self.attr2 = d['attr2'] - def export(self): - d = {'attr1': self.attr1, 'attr2': self.attr2} - return d + def to_dict(self): + return {'attr1': self.attr1, 'attr2': self.attr2} obj = Obj() sess = PoorSession(app.secret_key) - sess.data['class'] = obj.__class__ # write to cookie - sess.data['dict'] = obj.export() + sess.data['name'] = obj.__class__.__name__ # write to cookie + sess.data['dict'] = obj.to_dict() - obj = sess.data['class']() # read from cookie - obj.import(sess.data['dict']) + obj = globals()[sess.data['name']]() # read from cookie + obj.from_dict(sess.data['dict']) """ def __init__(self, secret_key: Union[Request, str, bytes], @@ -165,49 +164,50 @@ def __init__(self, secret_key: Union[Request, str, bytes], """Constructor. Arguments: - expires : int - Cookie ``Expires`` time in seconds, if it 0, no expire is set - max_age : int + expires + Cookie ``Expires`` time in seconds. If it is 0, no expiration + is set. + max_age Cookie ``Max-Age`` attribute. If both expires and max-age are set, max_age has precedence. - domain : str - Cookie ``Host`` to which the cookie will be sent. - path : str - Cookie ``Path`` that must exist in the requested URL. - secure : bool - If ``Secure`` cookie attribute will be sent. - same_site: str - The ``SameSite`` attribute. When is set could be one of - ``Strict|Lax|None``. By default attribute is not set which is - ``Lax`` by browser. - compress : compress module or class. - Could be ``bz2``, ``gzip.zlib``, or any other, which have - standard compress and decompress methods. Or it could be + domain + The cookie ``Host`` to which the cookie will be sent. + path + The cookie ``Path`` that must exist in the requested URL. + secure + If the ``Secure`` cookie attribute will be sent. + same_site + The ``SameSite`` attribute. When set, it can be one of + ``Strict|Lax|None``. By default, the attribute is not + set, which browsers default to ``Lax``. + compress + Can be ``bz2``, ``gzip.zlib``, or any other, which has + standard compress and decompress methods. Or it can be ``None`` to not use any compressing method. - sid : str - Cookie key name. + sid + The cookie key name. - .. code:: Python + .. code:: python session_config = { 'expires': 3600, # one hour 'max_age': 3600, 'domain': 'example.net', 'path': '/application', - ̈́'secure': True, + 'secure': True, 'same_site': True, 'compress': gzip, 'sid': 'MYSID' } - session = PostSession(app.secret_key, **config) + session = PoorSession(app.secret_key, **session_config) try: session.load(req.cookies) except SessionError as err: log.error("Invalid session: %s", str(err)) - *Changed in version 2.4.x*: use app.secret_key in constructor, and than - call load method. + *Changed in version 2.4.x*: Use app.secret_key in the + constructor, and then call the load method. """ if not isinstance(secret_key, (str, bytes)): # backwards compatibility log.warning('Do not use request in PoorSession constructor, ' @@ -236,7 +236,7 @@ def __init__(self, secret_key: Union[Request, str, bytes], self.load(secret_key.cookies) def load(self, cookies: Optional[SimpleCookie]): - """Load session from request's cookie""" + """Loads the session from the request's cookie.""" if not isinstance(cookies, SimpleCookie) or self.__sid not in cookies: return raw = cookies[self.__sid].value @@ -254,9 +254,9 @@ def load(self, cookies: Optional[SimpleCookie]): raise SessionError("Cookie data is not dictionary!") def write(self): - """Store data to cookie value. + """Stores data to the cookie value. - This method is called automatically in header method. + This method is called automatically in the header method. """ raw = b64encode(self.__cps.compress(hidden(dumps(self.data), self.__secret_key), 9)) @@ -280,9 +280,10 @@ def write(self): return raw def destroy(self): - """Destroy session. In fact, set cookie expires value to past (-1). + """Destroys the session by setting the cookie's expires value + to the past (-1). - Be sure, that data can't be changed: + Ensures that data cannot be changed: https://stackoverflow.com/a/5285982/8379994 """ self.cookie[self.__sid]['expires'] = -1 @@ -293,12 +294,12 @@ def destroy(self): self.cookie[self.__sid]['Secure'] = True def header(self, headers: Optional[Union[Headers, Response]] = None): - """Generate cookie headers and append it to headers if it set. + """Generates cookie headers and appends them to headers if set. - Returns list of cookie header pairs. + Returns a list of cookie header pairs. - headers : Headers or Response - Object, which is used to write header directly. + headers + The object used to write the header directly. """ self.write() cookies = self.cookie.output().split('\r\n') diff --git a/poorwsgi/state.py b/poorwsgi/state.py index 71b4d0a..ce378ea 100644 --- a/poorwsgi/state.py +++ b/poorwsgi/state.py @@ -1,4 +1,4 @@ -"""Constants like http status code and method types.""" +"""Constants for HTTP status codes and method types.""" # pylint: disable=consider-using-f-string @@ -9,7 +9,8 @@ __author__ = "Ondrej Tuma (McBig) " __date__ = "14 Oct 2024" -__version__ = "2.7.0" # https://www.python.org/dev/peps/pep-0386/ +# PEP 0386 -- Version Identification and Dependency Specification +__version__ = "2.7.0" DECLINED = 0 @@ -115,7 +116,7 @@ def deprecated(reason=""): - """Deprecated decorator.""" + """A decorator to mark functions or methods as deprecated.""" def wrapper(fun): @wraps(fun) def wrapped(*args, **kwargs): diff --git a/poorwsgi/wsgi.py b/poorwsgi/wsgi.py index 7383bcd..aee3a11 100644 --- a/poorwsgi/wsgi.py +++ b/poorwsgi/wsgi.py @@ -1,4 +1,5 @@ -"""Application callable class, which is the main point for wsgi application. +"""Application callable class, which is the main entry point for a WSGI +application. :Classes: Application :Functions: to_response @@ -8,48 +9,67 @@ # pylint: disable=unsubscriptable-object # pylint: disable=consider-using-f-string -from os import path, access, R_OK, environ +import re +import uuid from collections import OrderedDict -from logging import getLogger from hashlib import md5, sha256 -from typing import Union, Callable, Optional, Type, ClassVar +from logging import getLogger +from os import R_OK, access, environ, path from time import time +from typing import Callable, ClassVar, Optional, Type, Union -import re -import uuid - -from poorwsgi.state import \ - METHOD_GET, METHOD_POST, METHOD_HEAD, methods, \ - HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND, HTTP_FORBIDDEN, \ - deprecated from poorwsgi.request import Request, SimpleRequest -from poorwsgi.results import default_states, not_implemented, \ - internal_server_error, directory_index, debug_info -from poorwsgi.response import BaseResponse, HTTPException, \ - FileObjResponse, FileResponse, make_response, ResponseError +from poorwsgi.response import ( + BaseResponse, + FileObjResponse, + FileResponse, + HTTPException, + ResponseError, + make_response, +) +from poorwsgi.results import ( + debug_info, + default_states, + directory_index, + internal_server_error, + not_implemented, +) +from poorwsgi.state import ( + HTTP_FORBIDDEN, + HTTP_METHOD_NOT_ALLOWED, + HTTP_NOT_FOUND, + METHOD_GET, + METHOD_HEAD, + METHOD_POST, + deprecated, + methods, +) log = getLogger("poorwsgi") # check, if there is define filter in uri -re_filter = re.compile(r'<(\w+)(:[^>]+)?>') +re_filter = re.compile(r"<(\w+)(:[^>]+)?>") # check for invalid route definitions with spaces -# Matches: <{space}name, , -re_invalid_filter = re.compile(r'<\s+\w+|<\w+\s+[:|>]|<\w+:\s+\w+|<\w+:[^>]+\s+>') +# Matches: <{space}name, , +re_invalid_filter = re.compile( + r"<\s+\w+|<\w+\s+[:|>]|<\w+:\s+\w+|<\w+:[^>]+\s+>" +) # Supported authorization algorithms AUTH_DIGEST_ALGORITHMS = { - 'MD5': md5, - 'MD5-sess': md5, - 'SHA-256': sha256, - 'SHA-256-sess': sha256, + "MD5": md5, + "MD5-sess": md5, + "SHA-256": sha256, + "SHA-256-sess": sha256, # 'SHA-512-256': sha512, # Need extend library # 'SHA-512-256-sess': sha512, } def to_response(response): - """handler response to application response.""" + """Converts a handler's response to an application response.""" if isinstance(response, BaseResponse): return response @@ -58,23 +78,24 @@ def to_response(response): return make_response(*response) -class Application(): - """Poor WSGI application which is called by WSGI server. +class Application: + """Poor WSGI application that is called by the WSGI server. - Working of is describe in PEP 0333. This object store route dispatch table, - and have methods for it's using and of course __call__ method for use - as WSGI application. + Its working is described in PEP 0333. This object stores the route + dispatch table, and has methods for its use, as well as a __call__ + method for use as a WSGI application. """ + # pylint: disable=too-many-public-methods __instances: ClassVar[list[str]] = [] def __init__(self, name="__main__"): - """Application class is per name singleton. + """The Application class is a per-name singleton. - That means, there could be exist only one instance with same name. + That means, there can only be one instance with the same name. """ if Application.__instances.count(name): - raise RuntimeError('Application with name %s exist yet.' % name) + raise RuntimeError("Application with name %s exist yet." % name) Application.__instances.append(name) # Application name @@ -91,14 +112,17 @@ def __init__(self, name="__main__"): self.__handlers = {} self.__filters = { - ':int': (r'-?\d+', int), - ':float': (r'-?\d+(\.\d+)?', float), - ':word': (r'\w+', str), - ':hex': (r'[0-9a-fA-F]+', str), - ':uuid': (r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-' - r'[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', uuid.UUID), - ':re:': (None, str), - 'none': (r'[^/]+', str) + ":int": (r"-?\d+", int), + ":float": (r"-?\d+(\.\d+)?", float), + ":word": (r"\w+", str), + ":hex": (r"[0-9a-fA-F]+", str), + ":uuid": ( + r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" + r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", + uuid.UUID, + ), + ":re:": (None, str), + "none": (r"[^/]+", str), } # handlers of regex paths: {r'/user/([a-z]?)': {METHOD_GET: handler}} @@ -112,33 +136,34 @@ def __init__(self, name="__main__"): # -- Application variable self.__config = { - 'auto_args': True, - 'auto_form': True, - 'auto_json': True, - 'auto_data': True, - 'cached_size': 65365, - 'data_size': 65365, - 'read_timeout': 10, - 'keep_blank_values': 0, - 'strict_parsing': 0, - 'file_callback': None, - 'json_mime_types': [ - 'application/json', - 'application/javascript', - 'application/merge-patch+json'], - 'form_mime_types': [ - 'application/x-www-form-urlencoded', - 'multipart/form-data' + "auto_args": True, + "auto_form": True, + "auto_json": True, + "auto_data": True, + "cached_size": 65365, + "data_size": 65365, + "read_timeout": 10, + "keep_blank_values": 0, + "strict_parsing": 0, + "file_callback": None, + "json_mime_types": [ + "application/json", + "application/javascript", + "application/merge-patch+json", ], - 'auto_cookies': True, - 'debug': 'Off', - 'document_root': '', - 'document_index': 'Off', - 'secret_key': None, - 'auth_type': None, - 'auth_algorithm': 'MD5-sess', - 'auth_qop': 'auth', - 'auth_timeout': 300, + "form_mime_types": [ + "application/x-www-form-urlencoded", + "multipart/form-data", + ], + "auto_cookies": True, + "debug": "Off", + "document_root": "", + "document_index": "Off", + "secret_key": None, # nosec + "auth_type": None, + "auth_algorithm": "MD5-sess", + "auth_qop": "auth", + "auth_timeout": 300, } self.__auth_hash = md5 @@ -156,60 +181,63 @@ def __regex(self, match): if _filter in self.__filters: regex = self.__filters[_filter][0] - elif _filter[:4] == ':re:': # :re: filter have user defined regex + elif _filter[:4] == ":re:": # :re: filter have user defined regex regex = _filter[4:] else: try: regex = self.__filters[_filter][0] except KeyError as err: - raise RuntimeError("Undefined route group filter '%s'" % - _filter) from err + raise RuntimeError( + "Undefined route group filter '%s'" % _filter + ) from err return "(?P<%s>%s)" % (groups[0], regex) def __converter(self, _filter): _filter = str(_filter).lower() - _filter = ':re:' if _filter[:4] == ':re:' else _filter + _filter = ":re:" if _filter[:4] == ":re:" else _filter try: return self.__filters[_filter][1] except KeyError as err: - raise RuntimeError("Undefined route group filter '%s'" % - _filter) from err + raise RuntimeError( + "Undefined route group filter '%s'" % _filter + ) from err @property def name(self): - """Return application name.""" + """Returns the application name.""" return self.__name @property def filters(self): - """Copy of filter table. + """A copy of the filter table. - Filter table contains regular expressions and convert functions, + The filter table contains regular expressions and conversion functions; see Application.set_filter and Application.route. Default filters are: - **:int** match number and convert it to int + **:int** matches a number and converts it to an int - **:float** match number and convert it to float + **:float** matches a number and converts it to a float - **:word** match one string word + **:word** matches a single string word - **:hex** match hexadecimal value and convert it to str + **:hex** matches a hexadecimal value and converts it to a str - **:re:** match user defined regular expression + **:re:** matches a user-defined regular expression - **none** match any string without '/' character + **none** matches any string without the '/' character - For more details see `/debug-info` page of your application, where - you see all filters with regular expression definition. + For more details, see the `/debug-info` page of your application, + where you can find all filters with their regular expression + definitions. """ return self.__filters.copy() @property def before(self): - """Tuple of table with before-response handlers. + """A tuple of before-response handlers. See Application.before_response. """ @@ -217,7 +245,7 @@ def before(self): @property def after(self): - """Tuple of table with after-response handlers. + """A tuple of after-response handlers. See Application.after_response. """ @@ -225,15 +253,15 @@ def after(self): @property def defaults(self): - """Copy of table with default handlers. + """A copy of the table with default handlers. - See Application.set_default + See Application.set_default. """ return self.__dhandlers.copy() @property def routes(self): - """Copy of table with static handlers. + """A copy of the table with static handlers. See Application.route. """ @@ -241,7 +269,7 @@ def routes(self): @property def regular_routes(self): - """Copy of table with regular expression handlers. + """A copy of the table with regular expression handlers. See Application.route and Application.regular_route. """ @@ -249,267 +277,269 @@ def regular_routes(self): @property def states(self): - """Copy of table with http state handlers. + """A copy of the table with HTTP state handlers. - See Application.http_state + See Application.http_state. """ return self.__shandlers.copy() @property def errors(self): - """Copy of table with exception handlers. + """A copy of the table with exception handlers. - See Application.error_handler + See Application.error_handler. """ return self.__ehandlers.copy() @property def auto_args(self): - """Automatic parsing request arguments from uri. + """Automatic parsing of request arguments from the URI. - If it is True (default), Request object do automatic parsing request - uri to its args variable. + If it is True (default), the Request object automatically parses + the request URI into its args variable. """ - return self.__config['auto_args'] + return self.__config["auto_args"] @auto_args.setter def auto_args(self, value): - self.__config['auto_args'] = bool(value) + self.__config["auto_args"] = bool(value) @property def auto_form(self): - """Automatic parsing arguments from request body. + """Automatic parsing of arguments from the request body. - If it is True (default) and method is POST, PUT or PATCH, and - request mime type is one of form_mime_types, Request - object do automatic parsing request body to its form variable. + If it is True (default) and the method is POST, PUT, or PATCH, and the + request MIME type is one of form_mime_types, the Request + object automatically parses the request body into its form variable. """ - return self.__config['auto_form'] + return self.__config["auto_form"] @auto_form.setter def auto_form(self, value): - self.__config['auto_form'] = bool(value) + self.__config["auto_form"] = bool(value) @property def auto_json(self): - """Automatic parsing JSON from request body. + """Automatic parsing of JSON from the request body. - If it is True (default), method is POST, PUT or PATCH and request - mime type is one of json_mime_types, Request object do - automatic parsing request body to json variable. + If it is True (default), the method is POST, PUT, or PATCH, and + the request MIME type is one of json_mime_types, the Request + object automatically parses the request body into the json + variable. """ - return self.__config['auto_json'] + return self.__config["auto_json"] @auto_json.setter def auto_json(self, value): - self.__config['auto_json'] = bool(value) + self.__config["auto_json"] = bool(value) @property def auto_data(self): - """Enabling Request.data property for smaller requests. + """Enables the Request.data property for smaller requests. Default value is True. """ - return self.__config['auto_data'] + return self.__config["auto_data"] @auto_data.setter def auto_data(self, value: Union[int, bool]): - self.__config['auto_data'] = bool(value) + self.__config["auto_data"] = bool(value) @property def cached_size(self): - """Enabling cached_size for faster POST request. + """Enables cached_size for faster POST requests. Default value is 65365. """ - return self.__config['cached_size'] + return self.__config["cached_size"] @cached_size.setter def cached_size(self, value: int): - self.__config['cached_size'] = value + self.__config["cached_size"] = value @property def data_size(self): - """Size limit for Request.data property. + """Size limit for the Request.data property. - This value is which is compare to request Content-Type. Default value - is 32768 as 30Kb. + This value is compared to the request's Content-Length. The + default value is 32768 (30KB). """ - return self.__config['data_size'] + return self.__config["data_size"] @data_size.setter def data_size(self, value: int): - self.__config['data_size'] = int(value) + self.__config["data_size"] = int(value) @property def auto_cookies(self): - """Automatic parsing cookies from request headers. + """Automatic parsing of cookies from request headers. - If it is True (default) and Cookie request header was set, - SimpleCookie object was parsed to Request property cookies. + If it is True (default) and the Cookie request header is set, + a SimpleCookie object is parsed to the Request property cookies. """ - return self.__config['auto_cookies'] + return self.__config["auto_cookies"] @auto_cookies.setter def auto_cookies(self, value: Union[int, bool]): - self.__config['auto_cookies'] = bool(value) + self.__config["auto_cookies"] = bool(value) @property def debug(self): - """Application debug as another way how to set poor_Debug. + """Application debug mode, as another way to set poor_Debug. - This setting will be rewrite by poor_Debug environment variable. + This setting will be overridden by the poor_Debug environment variable. """ - return self.__config['debug'] == 'On' + return self.__config["debug"] == "On" @debug.setter def debug(self, value: Union[int, bool]): - self.__config['debug'] = 'On' if bool(value) else 'Off' + self.__config["debug"] = "On" if bool(value) else "Off" @property def document_root(self): - """Application document_root as another way how to set + """The application's document_root, as another way to set poor_DocumentRoot. - This setting will be rewrite by poor_DocumentRoot environ variable. + This setting will be overridden by the poor_DocumentRoot environment + variable. """ - return self.__config['document_root'] + return self.__config["document_root"] @document_root.setter def document_root(self, value: str): - self.__config['document_root'] = value + self.__config["document_root"] = value @property def document_index(self): - """Application document_root as another way how to set - poor_DocumentRoot. + """The application's document_index, as another way to set + poor_DocumentIndex. - This setting will be rewrite by poor_DocumentRoot environ variable. + This setting will be overridden by the poor_DocumentIndex environment + variable. """ - return self.__config['document_index'] == 'On' + return self.__config["document_index"] == "On" @document_index.setter def document_index(self, value: Union[int, bool]): - self.__config['document_index'] = 'On' if bool(value) else 'Off' + self.__config["document_index"] = "On" if bool(value) else "Off" @property def secret_key(self): - """Application secret_key could be replace by poor_SecretKey in - request. + """The application's secret_key can be overridden by poor_SecretKey in + the request. - Secret key is used by PoorSession class. It is generate from - some server variables, and the best way is set to your own long - key.""" - return self.__config['secret_key'] + The secret key is used by the PoorSession class. It is generated from + some server variables; it is best to set it to your own long key.""" + return self.__config["secret_key"] @secret_key.setter def secret_key(self, value: Union[str, bytes]): - self.__config['secret_key'] = value + self.__config["secret_key"] = value @property def keep_blank_values(self): - """Keep blank values in request arguments. + """Keeps blank values in request arguments. - If it is 1 (0 is default), automatic parsing request uri or body - keep blank values as empty string. + If it is 1 (0 is default), automatic parsing of the request URI or body + will keep blank values as empty strings. """ - return self.__config['keep_blank_values'] + return self.__config["keep_blank_values"] @keep_blank_values.setter def keep_blank_values(self, value: Union[int, bool]): - self.__config['keep_blank_values'] = int(value) + self.__config["keep_blank_values"] = int(value) @property def strict_parsing(self): - """Strict parse request arguments. + """Strict parsing of request arguments. - If it is 1 (0 is default), automatic parsing request uri or body - raise with exception on parsing error. + If it is 1 (0 is default), automatic parsing of the request URI or body + will raise an exception on a parsing error. """ - return self.__config['strict_parsing'] + return self.__config["strict_parsing"] @strict_parsing.setter def strict_parsing(self, value: Union[int, bool]): - self.__config['strict_parsing'] = int(value) + self.__config["strict_parsing"] = int(value) @property def file_callback(self): - """File callback use as parameter when parsing request body. + """A file callback used as a parameter when parsing the request body. - Default is None. Values could be a class or factory which got - filename from request body and have file compatible interface. + Default is None. The value can be a class or factory that receives a + filename from the request body and has a file-compatible interface. """ - return self.__config['file_callback'] + return self.__config["file_callback"] @file_callback.setter def file_callback(self, value: Callable): - self.__config['file_callback'] = value + self.__config["file_callback"] = value @property def read_timeout(self): - """Gets a timeout (in seconds) used for file receiving""" + """Gets the timeout (in seconds) used for file reception.""" return self.__config["read_timeout"] @read_timeout.setter def read_timeout(self, timeout: float): - """Sets a timeout (in seconds) used for file receiving""" + """Sets the timeout (in seconds) used for file reception.""" self.__config["read_timeout"] = timeout @property def json_mime_types(self): - """Copy of json mime type list. + """A copy of the JSON MIME type list. - Contains list of strings as json mime types, which is use for - testing, when automatics Json object is create from request body. + Contains a list of strings as JSON MIME types, which are used for + testing when an automatic JSON object is created from the request body. """ - return self.__config['json_mime_types'] + return self.__config["json_mime_types"] @property def auth_type(self): """Authorization type. - Only ``Digest`` type is supported now. + Only the ``Digest`` type is currently supported. """ - return self.__config['auth_type'] + return self.__config["auth_type"] @auth_type.setter def auth_type(self, value: str): value = value.capitalize() - if value not in ('Digest',): - raise ValueError('Unsupported authorization type') + if value not in ("Digest",): + raise ValueError("Unsupported authorization type") # for Digest - if self.__config['secret_key'] is None: - raise ValueError('Set secret key first') - self.__config['auth_type'] = value + if self.__config["secret_key"] is None: + raise ValueError("Set secret key first") + self.__config["auth_type"] = value @property def auth_algorithm(self): """Authorization algorithm. - Algorithm depends on authorization type and client support. + The algorithm depends on the authorization type and client support. Supported: :Digest: MD5 | MD5-sess | SHA256 | SHA256-sess :default: MD5-sess """ - return self.__config['auth_algorithm'] + return self.__config["auth_algorithm"] @auth_algorithm.setter def auth_algorithm(self, value: str): - if self.__config['auth_algorithm'] is None: - raise ValueError('Set authorization type first') + if self.__config["auth_algorithm"] is None: + raise ValueError("Set authorization type first") - if self.__config['auth_algorithm'] == 'Digest': + if self.__config["auth_algorithm"] == "Digest": if value not in AUTH_DIGEST_ALGORITHMS: - raise ValueError('Unsupported Digest algorithm') - self.__config['auth_algorithm'] = value + raise ValueError("Unsupported Digest algorithm") + self.__config["auth_algorithm"] = value self.__auth_hash = AUTH_DIGEST_ALGORITHMS[value] @property def auth_hash(self): - """Return authorization hash function. + """Returns the authorization hash function. - Function can be changed by auth_algorithm property. + The function can be changed by the auth_algorithm property. :default: md5 """ @@ -519,76 +549,78 @@ def auth_hash(self): def auth_qop(self): """Authorization quality of protection. - This is use for Digest authorization only. When browsers - supports only ``auth`` or empty value, PoorWSGI supports the same. + This is used for Digest authorization only. PoorWSGI supports only + ``auth`` or an empty value, consistent with common browser support. :default: auth """ - return self.__config['auth_qop'] + return self.__config["auth_qop"] @auth_qop.setter def auth_qop(self, value: str): - if value not in ('', 'auth', None): - raise ValueError('Unsupported quality of protection') - self.__config['auth_qop'] = value + if value not in ("", "auth", None): + raise ValueError("Unsupported quality of protection") + self.__config["auth_qop"] = value @property def auth_timeout(self): - """Digest Authorization timeout of nonce value in seconds. + """Digest Authorization timeout for the nonce value in seconds. - In fact, timeout will be between timeout and 2*timeout, because + In fact, the timeout will be between timeout and 2*timeout because time alignment is used. If timeout is None or 0, no timeout is used. - :default: 300 (5min) + :default: 300 (5 min) """ - return self.__config['auth_timeout'] + return self.__config["auth_timeout"] @auth_timeout.setter def auth_timeout(self, value: Optional[int]): if not isinstance(value, (type(None), int)): - raise ValueError('Unsupported auth_timeout value') - self.__config['auth_timeout'] = value + raise ValueError("Unsupported auth_timeout value") + self.__config["auth_timeout"] = value @property def form_mime_types(self): - """Copy of form mime type list. + """A copy of the form MIME type list. - Contains list of strings as form mime types, which is use for - testing, when automatics Form object is create from request body. + Contains a list of strings as form MIME types, which are used for + testing when an automatic Form object is created from the request body. """ - return self.__config['form_mime_types'] + return self.__config["form_mime_types"] def set_filter(self, name: str, regex: str, converter: Callable = str): - r"""Create new filter or overwrite built-ins. + r"""Creates a new filter or overwrites built-ins. - name : str - Name of filter which is used in route or set_route method. - regex : str - Regular expression which used for filter. - converter : function - Converter function or class, which gets string in input. Default is - str function, which call __str__ method on input object. + name + The name of the filter used in the route or set_route method. + regex + The regular expression used for the filter. + converter + The converter function or class that takes a string as input. + The default is the str function, which calls the __str__ + method on the input object. .. code:: python app.set_filter('uint', r'\d+', int) """ - name = ':'+name if name[0] != ':' else name + name = ":" + name if name[0] != ":" else name self.__filters[name] = (regex, converter) @deprecated("use before_response instead") def before_request(self): - """Deprecated, use before_request instead.""" + """Deprecated; use before_response instead.""" def wrapper(fun): self.add_before_response(fun) return fun + return wrapper def before_response(self): - """Append handler to call before each response. + """Appends a handler to call before each response. - This is decorator for function to call before each response. + This is a decorator for a function to call before each response. .. code:: python @@ -596,21 +628,23 @@ def before_response(self): def before_each_response(req): print("Response coming") """ + def wrapper(fun): self.add_before_response(fun) return fun + return wrapper @deprecated("use add_before_response instead") def add_before_request(self, fun: Callable): - """Deprecated, use add_before_response instead.""" + """Deprecated; use add_before_response instead.""" self.add_before_response(fun) def add_before_response(self, fun: Callable): - """Append handler to call before each response. + """Appends a handler to call before each response. - Method adds function to list functions which is call before each - response. + This method adds a function to the list of functions that are + called before each response. .. code:: python @@ -625,28 +659,31 @@ def before_each_response(req): @deprecated("use pop_before_response instead") def pop_before_request(self, fun: Callable): - """Deprecated, use pop_before_response instead.""" + """Deprecated; use pop_before_response instead.""" self.pop_before_response(fun) def pop_before_response(self, fun: Callable): - """Remove handler added by add_before_response or before_response.""" + """Removes a handler added by add_before_response or + before_response.""" if not self.__before.count(fun): raise ValueError("%s is not in list" % str(fun)) self.__before.remove(fun) @deprecated("use after_response instead") def after_request(self): - """Deprecated, use after_response instead.""" + """Deprecated; use after_response instead.""" + def wrapper(fun): self.add_after_response(fun) return fun + return wrapper def after_response(self): - """Append handler to call after each response. + """Appends a handler to call after each response. - This decorator append function to be called after each response, - if you want to use it redefined all outputs. + This decorator appends a function to be called after each response. + The handler must return a response object. .. code:: python @@ -655,21 +692,23 @@ def after_each_response(request, response): print("Response out") return response """ + def wrapper(fun): self.add_after_response(fun) return fun + return wrapper @deprecated("use add_after_response instead") def add_after_request(self, fun: Callable): - """Deprecated, use add_after_response instead.""" + """Deprecated; use add_after_response instead.""" self.add_after_response(fun) def add_after_response(self, fun: Callable): - """Append handler to call after each response. + """Appends a handler to call after each response. - Method for direct append function to list functions which are called - after each response. + This method directly appends a function to the list of functions + that are called after each response. .. code:: python @@ -685,41 +724,44 @@ def after_each_response(request, response): @deprecated("use pop_after_response instead") def pop_after_request(self, fun: Callable): - """Deprecated, use pop_after_response instead.""" + """Deprecated; use pop_after_response instead.""" self.pop_after_response(fun) def pop_after_response(self, fun: Callable): - """Remove handler added by add_after_response or after_response.""" + """Removes a handler added by add_after_response or after_response.""" if not self.__before.count(fun): raise ValueError("%s is not in list" % str(fun)) self.__after.remove(fun) def default(self, method: int = METHOD_HEAD | METHOD_GET): - """Set default handler. + """Sets a default handler. - This is decorator for default handler for http method (called before - error_not_found). + This is a decorator for a default handler for an HTTP method + (called before error_not_found). .. code:: python @app.default(METHOD_GET_POST) def default_get_post(req): - # this function will be called if no uri match in internal - # uri table with method. It's similar like not_found error, - # but without error + # Called if no URI matches in the internal URI table for + # the method. Similar to a not_found error, but without + # an error. ... """ + def wrapper(fun): self.set_default(fun, method) return fun + return wrapper - # enddef - def set_default(self, fun: Callable, - method: int = METHOD_HEAD | METHOD_GET): - """Set default handler. + def set_default( + self, fun: Callable, method: int = METHOD_HEAD | METHOD_GET + ): + """Sets a default handler. - Set fun default handler for http method called before error_not_found. + Sets ``fun`` as the default handler for the HTTP method, called before + error_not_found. .. code:: python @@ -728,21 +770,20 @@ def set_default(self, fun: Callable, for val in methods.values(): if method & val: self.__dhandlers[val] = fun - # enddef def pop_default(self, method: int): - """Pop default handler for method.""" + """Pops the default handler for a method.""" return self.__dhandlers.pop(method) def route(self, uri: str, method: int = METHOD_HEAD | METHOD_GET): - r"""Wrap function to be handler for uri and specified method. + r"""Wraps a function to be a handler for a URI and specified method. - You can define uri as static path or as groups which are hand - to handler as next parameters. + You can define the URI as a static path or with groups, which are + passed to the handler as subsequent parameters. .. code:: python - # static uri + # static URI @app.route('/user/post', method=METHOD_POST) def user_create(req): ... @@ -757,40 +798,42 @@ def user_detail(req, name): def surnames_by_age(req, surname, age): ... - # group with own regular expression filter + # group with its own regular expression filter @app.route('//') def car(req, car, color): ... - If you can use some name of group which is python keyword, like class, - you can use \**kwargs syntax: + If you need to use a group name that is a Python keyword, such as + 'class', you can use the ``**kwargs`` syntax: .. code:: python @app.route('//') def classes(req, **kwargs): - return ("'%s' class is %d lenght." % + return ("'%s' class is %d length." % (kwargs['class'], kwargs['len'])) - Be sure with ordering of call this decorator or set_route function with - groups regular expression. Regular expression routes are check with the - same ordering, as you create internal table of them. First match stops - any other searching. In fact, if groups are detect, they will be - transfer to normal regular expression, and will be add to second - internal table. + Be mindful of the ordering when calling this decorator or the set_route + function with group regular expressions. Regular expression routes are + checked in the same order as they are created in the internal table. + The first match stops any further searching. In fact, if groups are + detected, they will be transferred to normal regular expressions and + added to a second internal table. """ + def wrapper(fun): self.set_route(uri, fun, method) return fun + return wrapper - # enddef - def set_route(self, uri: str, fun: Callable, - method: int = METHOD_HEAD | METHOD_GET): - """Set handler for uri and method. + def set_route( + self, uri: str, fun: Callable, method: int = METHOD_HEAD | METHOD_GET + ): + """Sets a handler for a URI and method. - Another way to add fun as handler for uri. See Application.route - documentation for details. + Another way to add ``fun`` as a handler for the URI. See + Application.route documentation for details. .. code:: python @@ -807,10 +850,11 @@ def set_route(self, uri: str, fun: Callable, raise ValueError(msg) if re_filter.search(uri): - r_uri = re_filter.sub(self.__regex, uri) + '$' - converters = tuple((g[0], self.__converter(g[1])) - for g in (m.groups() - for m in re_filter.finditer(uri))) + r_uri = re_filter.sub(self.__regex, uri) + "$" + converters = tuple( + (g[0], self.__converter(g[1])) + for g in (m.groups() for m in re_filter.finditer(uri)) + ) self.set_regular_route(r_uri, fun, method, converters, uri) else: if uri not in self.__handlers: @@ -820,34 +864,36 @@ def set_route(self, uri: str, fun: Callable, self.__handlers[uri][val] = fun def pop_route(self, uri: str, method: int): - """Pop handler for uri and method from handers table. + """Pops a handler for a URI and method from the handlers table. - Method must be define unique, so METHOD_GET_POST could not be use. - If you want to remove handler for both methods, you must call pop route - for each method state. + The method must be defined uniquely, so METHOD_GET_POST cannot be used. + If you want to remove a handler for both methods, you must call + pop_route for each method state. """ if re_filter.search(uri): - r_uri = re_filter.sub(self.__regex, uri) + '$' + r_uri = re_filter.sub(self.__regex, uri) + "$" return self.pop_regular_route(r_uri, method) handlers = self.__handlers.get(uri, {}) rval = handlers.pop(method) - if not handlers: # is empty + if not handlers: # is empty self.__handlers.pop(uri, None) return rval def is_route(self, uri: str): - """Check if uri have any registered record.""" + """Checks if the URI has any registered record.""" if re_filter.search(uri): - r_uri = re_filter.sub(self.__regex, uri) + '$' + r_uri = re_filter.sub(self.__regex, uri) + "$" return self.is_regular_route(r_uri) return uri in self.__handlers def regular_route(self, ruri: str, method: int = METHOD_HEAD | METHOD_GET): - r"""Wrap function to be handler for uri defined by regular expression. + r"""Wraps a function to be a handler for a URI defined by a regular + expression. - Both of function, regular_route and set_regular_route store routes - to special internal table, which is another to table of static routes. + Both regular_route and set_regular_route functions store routes + in a special internal table, which is separate from the table of static + routes. .. code:: python @@ -856,36 +902,44 @@ def regular_route(self, ruri: str, method: int = METHOD_HEAD | METHOD_GET): def any_user(req): ... - # regular expression with + # regular expression with named group @app.regular_route(r'/user/(?P\w+)') def user_detail(req, user): # named path args ... - Be sure with ordering of call this decorator or set_regular_route - function. Regular expression routes are check with the same ordering, - as you create internal table of them. First match stops any other - searching. + Be mindful of the ordering when calling this decorator or the + set_regular_route function. Regular expression routes are checked + in the same order as they are created in the internal table. The + first match stops any further searching. """ + def wrapper(fun): self.set_regular_route(ruri, fun, method) return fun + return wrapper - def set_regular_route(self, uri: str, fun: Callable, - method: int = METHOD_HEAD | METHOD_GET, - converters=(), rule: Optional[str] = None): - r"""Set handler for uri defined by regular expression. + def set_regular_route( + self, + uri: str, + fun: Callable, + method: int = METHOD_HEAD | METHOD_GET, + converters=(), + rule: Optional[str] = None, + ): + r"""Sets a handler for a URI defined by a regular expression. - Another way to add fn as handler for uri defined by regular expression. - See Application.regular_route documentation for details. + Another way to add ``fun`` as a handler for a URI defined by a + regular expression. See Application.regular_route documentation + for details. .. code:: python app.set_regular_route('/use/\w+/post', user_create, METHOD_POST) - This method is internally use, when groups are found in static route, - adding by route or set_route method. + This method is used internally when groups are found in a static route, + added by the route or set_route method. """ r_uri = re.compile(uri, re.U) if r_uri not in self.__rhandlers: @@ -895,25 +949,29 @@ def set_regular_route(self, uri: str, fun: Callable, self.__rhandlers[r_uri][val] = (fun, converters, rule) def pop_regular_route(self, uri: str, method: int): - """Pop handler and converters for uri and method from handlers table. + """Pops a handler and converters for a URI and method from the handlers + table. - For more details see Application.pop_route. + For more details, see Application.pop_route. """ r_uri = re.compile(uri, re.U) handlers = self.__rhandlers.get(r_uri, {}) rval = handlers.pop(method) - if not handlers: # is empty + if not handlers: # is empty self.__rhandlers.pop(r_uri, None) return rval def is_regular_route(self, r_uri): - """Check if regular expression uri have any registered record.""" + """Checks if a regular expression URI has any registered record.""" r_uri = re.compile(r_uri, re.U) return r_uri in self.__rhandlers - def http_state(self, status_code: int, - method: int = METHOD_HEAD | METHOD_GET | METHOD_POST): - """Wrap function to handle http status codes. + def http_state( + self, + status_code: int, + method: int = METHOD_HEAD | METHOD_GET | METHOD_POST, + ): + """Wraps a function to handle HTTP status codes. .. code:: python @@ -921,14 +979,20 @@ def http_state(self, status_code: int, def page_not_found(req, *_): return "Your page %s was not found." % req.path, "text/plain" """ + def wrapper(fun): self.set_http_state(status_code, fun, method) return fun + return wrapper - def set_http_state(self, status_code: int, fun: Callable, - method: int = METHOD_HEAD | METHOD_GET | METHOD_POST): - """Set function as handler for http state code and method.""" + def set_http_state( + self, + status_code: int, + fun: Callable, + method: int = METHOD_HEAD | METHOD_GET | METHOD_POST, + ): + """Sets a function as the handler for an HTTP state code and method.""" if status_code not in self.__shandlers: self.__shandlers[status_code] = {} for val in methods.values(): @@ -936,18 +1000,20 @@ def set_http_state(self, status_code: int, fun: Callable, self.__shandlers[status_code][val] = fun def pop_http_state(self, status_code: int, method: int): - """Pop handler for http state and method. + """Pops a handler for an HTTP state and method. - As Application.pop_route, for pop multi-method handler, you must call - pop_http_state for each method. + Similar to Application.pop_route, to pop a multi-method handler, you + must call pop_http_state for each method. """ handlers = self.__shandlers.get(status_code, {}) return handlers.pop(method) def error_handler( - self, error: Type[Exception], - method: int = METHOD_HEAD | METHOD_GET | METHOD_POST): - """Wrap function to handle exceptions. + self, + error: Type[Exception], + method: int = METHOD_HEAD | METHOD_GET | METHOD_POST, + ): + """Wraps a function to handle exceptions. .. code:: python @@ -956,15 +1022,20 @@ def value_error(req, error): log.exception("ValueError %s", error) return "Values %s are not correct." % req.args, "text/plain" """ + def wrapper(fun): self.set_error_handler(error, fun, method) return fun + return wrapper def set_error_handler( - self, error: Type[Exception], fun: Callable, - method: int = METHOD_HEAD | METHOD_GET | METHOD_POST): - """Set function as handler for exception and method.""" + self, + error: Type[Exception], + fun: Callable, + method: int = METHOD_HEAD | METHOD_GET | METHOD_POST, + ): + """Sets a function as the handler for an exception and method.""" if error not in self.__ehandlers: self.__ehandlers[error] = {} for val in methods.values(): @@ -972,22 +1043,24 @@ def set_error_handler( self.__ehandlers[error][val] = fun def pop_error_handler(self, error: Type[Exception], method: int): - """Pop handler for http state and method. + """Pops a handler for an exception and method. - As Application.pop_route, for pop multi-method handler, you must call - pop_http_state for each method. + Similar to Application.pop_route, to pop a multi-method handler, + you must call pop_error_handler for each method. """ handlers = self.__ehandlers.get(error, {}) return handlers.pop(method) def state_from_table(self, req: SimpleRequest, status_code: int, **kwargs): - """Internal method, which is called if another http state has occurred. + """Internal method, which is called if another HTTP state has occurred. - If status code is in Application.shandlers (fill with http_state - function), call this handler. + If the status code is in Application.shandlers (filled with the + http_state function), this handler is called. """ - if status_code in self.__shandlers \ - and req.method_number in self.__shandlers[status_code]: + if ( + status_code in self.__shandlers + and req.method_number in self.__shandlers[status_code] + ): try: handler = self.__shandlers[status_code][req.method_number] req.error_handler = handler @@ -1007,12 +1080,11 @@ def state_from_table(self, req: SimpleRequest, status_code: int, **kwargs): return not_implemented(req, status_code) def error_from_table(self, req: SimpleRequest, error: Exception): - """Internal method, which is called when exception was raised.""" + """Internal method, which is called when an exception is raised.""" handler = None for error_type, hdls in self.__ehandlers.items(): - if isinstance(error, error_type) \ - and req.method_number in hdls: + if isinstance(error, error_type) and req.method_number in hdls: handler = hdls[req.method_number] break @@ -1028,7 +1100,8 @@ def error_from_table(self, req: SimpleRequest, error: Exception): status_code = http_err.args[0] kwargs = http_err.args[1] return to_response( - self.state_from_table(req, status_code, **kwargs)) + self.state_from_table(req, status_code, **kwargs) + ) except Exception: # pylint: disable=broad-except return internal_server_error(req) @@ -1036,30 +1109,32 @@ def error_from_table(self, req: SimpleRequest, error: Exception): def handler_from_default(self, req: SimpleRequest): """Internal method, which is called if no handler is found.""" - req.uri_rule = '/*' + req.uri_rule = "/*" if req.method_number in self.__dhandlers: req.uri_handler = self.__dhandlers[req.method_number] - self.handler_from_before(req) # call before handlers now + self.handler_from_before(req) # call before handlers now return self.__dhandlers[req.method_number](req) - self.handler_from_before(req) # call before handlers now + self.handler_from_before(req) # call before handlers now log.error("404 Not Found: %s %s", req.method, req.path) raise HTTPException(HTTP_NOT_FOUND) def handler_from_before(self, req: SimpleRequest): - """Internal method, which run all before (pre_proccess) handlers. + """Internal method, which runs all before (pre_process) handlers. - This method was call before end-point route handler. + This method is called before the endpoint route handler. """ for fun in self.__before: fun(req) def handler_from_table(self, req: Request): # noqa: C901 - """Call right handler from handlers table (fill with route function). + """Calls the correct handler from the handlers table (populated + by the route function). - If no handler is fined, try to find directory or file if Document Root, - resp. Document Index is set. Then try to call default handler for right - method or call handler for status code 404 - not found. + If no handler is found, it attempts to serve a file from Document Root + or a directory listing if Document Index is also enabled. Then it + attempts to call the default handler for the correct method or + calls the handler for status code 404 (Not Found). """ # pylint: disable=too-many-return-statements # static routes @@ -1070,7 +1145,7 @@ def handler_from_table(self, req: Request): # noqa: C901 req.uri_rule = req.path # nice variable for before handlers req.uri_handler = handler self.handler_from_before(req) # call before handlers now - return handler(req) # call right handler now + return handler(req) # call right handler now self.handler_from_before(req) # call before handlers now raise HTTPException(HTTP_METHOD_NOT_ALLOWED) @@ -1079,74 +1154,83 @@ def handler_from_table(self, req: Request): # noqa: C901 for ruri in self.__rhandlers: match = ruri.match(req.path) if match and req.method_number in self.__rhandlers[ruri]: - handler, converters, rule = \ - self.__rhandlers[ruri][req.method_number] + handler, converters, rule = self.__rhandlers[ruri][ + req.method_number + ] req.uri_rule = rule or ruri.pattern req.uri_handler = handler if converters: # create OrderedDict from match inside of dict for # converters applying req.path_args = OrderedDict( - (g, c(v))for ((g, c), v) in zip(converters, - match.groups())) - self.handler_from_before(req) # call before handlers now + (g, c(v)) + for ((g, c), v) in zip(converters, match.groups()) + ) + self.handler_from_before(req) # call before handlers now return handler(req, *req.path_args.values()) req.path_args = match.groupdict() - self.handler_from_before(req) # call before handlers now + self.handler_from_before(req) # call before handlers now return handler(req, *match.groups()) # try file or index - if req.document_root and \ - req.method_number & (METHOD_HEAD | METHOD_GET): - rfile = "%s%s" % (req.document_root, - path.normpath("%s" % req.path)) + if req.document_root and req.method_number & ( + METHOD_HEAD | METHOD_GET + ): + rfile = "%s%s" % ( + req.document_root, + path.normpath("%s" % req.path), + ) if not path.exists(rfile): - if req.debug and req.path == '/debug-info': # work if debug - req.uri_rule = '/debug-info' + if req.debug and req.path == "/debug-info": # work if debug + req.uri_rule = "/debug-info" req.uri_handler = debug_info self.handler_from_before(req) # call before handlers now return debug_info(req, self) - return self.handler_from_default(req) # try default + return self.handler_from_default(req) # try default # return file if path.isfile(rfile) and access(rfile, R_OK): - req.uri_rule = '/*' - self.handler_from_before(req) # call before handlers now + req.uri_rule = "/*" + self.handler_from_before(req) # call before handlers now log.info("Return file: %s", req.path) return FileResponse(rfile) # return directory index - if req.document_index and path.isdir(rfile) \ - and access(rfile, R_OK): + if ( + req.document_index + and path.isdir(rfile) + and access(rfile, R_OK) + ): log.info("Return directory: %s", req.path) - req.uri_rule = '/*' + req.uri_rule = "/*" req.uri_handler = directory_index - self.handler_from_before(req) # call before handlers now + self.handler_from_before(req) # call before handlers now return directory_index(req, rfile) - self.handler_from_before(req) # call before handlers now + self.handler_from_before(req) # call before handlers now raise HTTPException(HTTP_FORBIDDEN) # req.document_root - if req.debug and req.path == '/debug-info': - req.uri_rule = '/debug-info' + if req.debug and req.path == "/debug-info": + req.uri_rule = "/debug-info" req.uri_handler = debug_info - self.handler_from_before(req) # call before handlers now + self.handler_from_before(req) # call before handlers now return debug_info(req, self) return self.handler_from_default(req) def __request__(self, env, start_response): # noqa: C901 - """Create Request instance and return wsgi response. + """Creates a Request instance and returns a WSGI response. - This method create Request object, call handlers from - Application.before, uri handler (handler_from_table), default handler - (Application.defaults) or error handler (Application.state_from_table), - and handlers from Application.after. + This method creates a Request object, calls handlers from + Application.before, the URI handler (handler_from_table), the + default handler (Application.defaults) or the error handler + (Application.state_from_table), and handlers from + Application.after. """ # pylint: disable=method-hidden,too-many-branches,too-many-statements - env['REQUEST_STARTTIME'] = time() + env["REQUEST_STARTTIME"] = time() request = None try: @@ -1162,10 +1246,11 @@ def __request__(self, env, start_response): # noqa: C901 status_code = http_err.args[0] kwargs = http_err.args[1] response = to_response( - self.state_from_table(request, status_code, **kwargs)) + self.state_from_table(request, status_code, **kwargs) + ) except (ConnectionError, SystemExit) as err: log.warning(str(err)) - log.warning(' *** You should ignore next error ***') + log.warning(" *** You should ignore next error ***") return () except ResponseError: log.error("Bad returned value from %s", request.uri_handler) @@ -1188,13 +1273,16 @@ def __request__(self, env, start_response): # noqa: C901 response = internal_server_error(request) __fn = None - try: # call post_process handler + try: # call post_process handler for fun in self.__after: __fn = fun response = to_response(fun(request, response)) except BaseException as err: # pylint: disable=broad-except - log.error("Handler %s from %s returns invalid data or crashed", - __fn, __fn.__module__) + log.error( + "Handler %s from %s returns invalid data or crashed", + __fn, + __fn.__module__, + ) response = self.error_from_table(request, err) if not response: response = to_response(self.state_from_table(request, 500)) @@ -1202,49 +1290,58 @@ def __request__(self, env, start_response): # noqa: C901 skip_sendfile = request.server_software == "uWsgi" and response.ranges # need working fileno method try: - if isinstance(response, FileObjResponse) and \ - "wsgi.file_wrapper" in env and not skip_sendfile: - return env['wsgi.file_wrapper'](response(start_response)) - return response(start_response) # return bytes generator + if ( + isinstance(response, FileObjResponse) + and "wsgi.file_wrapper" in env + and not skip_sendfile + ): + return env["wsgi.file_wrapper"](response(start_response)) + return response(start_response) # return bytes generator except HTTPException as http_err: # HTTP_RANGE_NOT_SATISFIABLE case response = http_err.make_response() return response(start_response) def __call__(self, env, start_response): - """Callable define for Application instance. + """Callable defined for the Application instance. - This method run __request__ method. + This method runs the __request__ method. """ return self.__request__(env, start_response) def __profile_request__(self, env, start_response): """Profiler version of __request__. - This method is used if set_profile is used.""" + This method is used if set_profile is called. + """ + # pylint: disable=possibly-unused-variable def wrapper(rval): rval.append(self.__original_request__(env, start_response)) rval = [] - uri_dump = (self.__dump + "_" + env.get('REQUEST_METHOD') + - env.get('PATH_INFO').replace('/', '_') + - "." + str(time()) + - '.profile') - log.info('Generate %s', uri_dump) - self.__runctx('wrapper(rval)', globals(), locals(), filename=uri_dump) + uri_dump = ( + self.__dump + + "_" + + env.get("REQUEST_METHOD") + + env.get("PATH_INFO").replace("/", "_") + + "." + + str(time()) + + ".profile" + ) + log.info("Generate %s", uri_dump) + self.__runctx("wrapper(rval)", globals(), locals(), filename=uri_dump) return rval[0] - # enddef def __repr__(self): - return '%s - callable Application class instance' % self.__name + return "%s - callable Application class instance" % self.__name def set_profile(self, runctx, dump): - """Set profiler for __call__ function. + """Sets a profiler for the __call__ function. - runctx : function - function from profiler module - dump : str - path and prefix for .profile files + runctx + Function from the profiler module. + dump + Path and prefix for .profile files. Typical usage: @@ -1261,33 +1358,32 @@ def set_profile(self, runctx, dump): self.__original_request__ = self.__request__ self.__request__ = self.__profile_request__ - # enddef def del_profile(self): - """Remove profiler from application.""" + """Removes the profiler from the application.""" self.__request__ = self.__original_request__ @staticmethod def get_options(): - """Returns dictionary with application variables from system + """Returns a dictionary with application variables from the system environment. - Application variables start with ``app_`` prefix, - but in returned dictionary is set without this prefix. + Application variables start with the ``app_`` prefix, + but in the returned dictionary, they are set without this prefix. .. code:: python app_db_server = localhost # application variable db_server app_templates = app/templ # application variable templates - This method works like Request.get_options, but work with - os.environ, so it works only with wsgi servers, which set not only - request environ, but os.environ too. Apaches mod_wsgi don't do that, - uWsgi and PoorHTTP do that. + This method works like Request.get_options, but it works with + os.environ, so it is effective only with WSGI servers that set + not only the request environment, but also os.environ. Apache's + mod_wsgi does not do that; uWsgi and PoorHTTP do. """ options = {} for key, val in environ.items(): key = key.strip() - if key[:4].lower() == 'app_': + if key[:4].lower() == "app_": options[key[4:].lower()] = val.strip() return options diff --git a/setup.py b/setup.py index ba06ee3..706141f 100644 --- a/setup.py +++ b/setup.py @@ -186,7 +186,6 @@ def doc(): classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", "Natural Language :: English", "Natural Language :: Czech", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Operating System :: POSIX :: BSD", diff --git a/tests/test_application_error.py b/tests/test_application_error.py index a77ec54..c438940 100644 --- a/tests/test_application_error.py +++ b/tests/test_application_error.py @@ -1,4 +1,4 @@ -"""Unit test for unicode decode error propagation.""" +"""Unit test for Unicode decode error propagation.""" from io import BytesIO from time import time @@ -9,13 +9,14 @@ def test_keyerror_on_internal_error(monkeypatch): - """Test for KeyError: 'args' on unicode decode error in path.""" + """Tests for KeyError: 'args' on Unicode decode error in the path.""" app = Application() def mock_path(self): - """Mock of old Request.path property.""" + """Mock of the old Request.path property.""" # We are mocking the old behavior, where UnicodeDecodeError was not - # caught inside path property and propagated to __request__ method. + # caught inside the path property and propagated to the __request__ + # method. raise UnicodeDecodeError("utf-8", b"\xc0", 0, 1, "invalid start byte") monkeypatch.setattr(Request, "path", property(mock_path)) diff --git a/tests/test_digest.py b/tests/test_digest.py index 4ab9beb..292f6e5 100644 --- a/tests/test_digest.py +++ b/tests/test_digest.py @@ -62,37 +62,44 @@ def req(app): class TestMap(): - """Test for PasswordMap class.""" + """Tests for the PasswordMap class.""" def test_set(self, pmap): + """Tests setting a user's digest in the PasswordMap.""" assert REALM in pmap assert USER in pmap[REALM] assert pmap[REALM][USER] == DIGEST def test_delete(self, pmap): + """Tests deleting a user from the PasswordMap.""" assert pmap.delete(REALM, USER) is True assert pmap.delete(REALM, USER) is False def test_find(self, pmap): + """Tests finding a user's digest in the PasswordMap.""" assert pmap.find(REALM, USER) == DIGEST assert pmap.find('', USER) is None assert pmap.find(REALM, '') is None def test_verify(self, pmap): + """Tests verifying a user's digest in the PasswordMap.""" assert pmap.verify(REALM, USER, DIGEST) is True assert pmap.verify(REALM, USER, '') is False assert pmap.verify(REALM, '', DIGEST) is False assert pmap.verify('', USER, DIGEST) is False def test_load(self): + """Tests loading the PasswordMap from a file.""" pmap = PasswordMap(FILE) pmap.load() assert pmap.verify(REALM, USER, DIGEST) is True def test_hexdigest(): + """Tests the hexdigest function.""" assert hexdigest(USER, REALM, 'looser') == DIGEST def test_header_parsing(req): + """Tests parsing the Authorization header.""" assert req.authorization == DICT diff --git a/tests/test_header.py b/tests/test_header.py index 1e31e45..120a7b1 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -1,6 +1,6 @@ -"""Tests for request.Header class.""" +"""Tests for the request.Header class.""" -from unittest import TestCase +import pytest from poorwsgi.request import Headers @@ -8,10 +8,11 @@ # pylint: disable=no-self-use -class TestSetValues(TestCase): - """Adding headers and or setting header values.""" +class TestSetValues: + """Tests adding headers and setting header values.""" def test_constructor_empty(self): + """Tests the Headers constructor with empty inputs.""" Headers() Headers([]) # list Headers(tuple()) @@ -19,49 +20,62 @@ def test_constructor_empty(self): Headers(set()) def test_constructor_tuples(self): - headers = Headers([('X-Test', 'Ok'), ('Key', 'Value')]) - assert headers['X-Test'] == 'Ok' + """Tests the Headers constructor with tuple inputs.""" + headers = Headers([("X-Test", "Ok"), ("Key", "Value")]) + assert headers["X-Test"] == "Ok" - headers = Headers((('X-Test', 'Ok'), ('X-Test', 'Value'))) - assert headers['X-Test'] == 'Ok' - assert headers.get_all('X-Test') == ('Ok', 'Value') + headers = Headers((("X-Test", "Ok"), ("X-Test", "Value"))) + assert headers["X-Test"] == "Ok" + assert headers.get_all("X-Test") == ("Ok", "Value") def test_constructor_dict(self): - headers = Headers({'X-Test': 'Ok', 'Key': 'Value'}) - assert headers['X-Test'] == 'Ok' + """Tests the Headers constructor with dictionary inputs.""" + headers = Headers({"X-Test": "Ok", "Key": "Value"}) + assert headers["X-Test"] == "Ok" xheaders = Headers(headers.items()) - assert xheaders['X-Test'] == 'Ok' + assert xheaders["X-Test"] == "Ok" def test_constructor_error(self): - with self.assertRaises(TypeError): - Headers('Value') - with self.assertRaises(ValueError): - Headers(['a', 'b']) - with self.assertRaises(TypeError): - Headers({'None': None}) + """Tests the Headers constructor with invalid inputs, expecting + errors.""" + with pytest.raises(TypeError): + Headers("Value") + with pytest.raises(ValueError): # noqa: PT011 + Headers(["a", "b"]) + with pytest.raises(TypeError): + Headers({"None": None}) def test_set(self): + """Tests setting a header value using dictionary-like assignment.""" headers = Headers() - headers['X-Test'] = "Ok" - assert headers.items() == (('X-Test', 'Ok'),) + headers["X-Test"] = "Ok" + assert headers.items() == (("X-Test", "Ok"),) def test_add_header(self): + """Tests adding headers with various parameters using add_header.""" headers = Headers() - headers.add_header('Content-Disposition', 'attachment', - filename='image.png') - assert headers['Content-Disposition'] == \ - 'attachment; filename="image.png"' - - headers.add_header('Accept-Encoding', - (('gzip', 1.0), ('identity', 0.5), ('*', 0))) - assert headers['Accept-Encoding'] == \ - 'gzip;q=1.0, identity;q=0.5, *;q=0' - - headers.add_header('X-Test', key="value") - assert headers['X-Test'] == 'key="value"' + headers.add_header( + "Content-Disposition", "attachment", filename="image.png" + ) + assert ( + headers["Content-Disposition"] + == 'attachment; filename="image.png"' + ) + + headers.add_header( + "Accept-Encoding", (("gzip", 1.0), ("identity", 0.5), ("*", 0)) + ) + assert ( + headers["Accept-Encoding"] == "gzip;q=1.0, identity;q=0.5, *;q=0" + ) + + headers.add_header("X-Test", key="value") + assert headers["X-Test"] == 'key="value"' def test_add_header_error(self): + """Tests adding a header with invalid values, expecting a + ValueError.""" headers = Headers() - with self.assertRaises(ValueError): - headers.add_header('X-None') + with pytest.raises(ValueError): # noqa: PT011 + headers.add_header("X-None") diff --git a/tests/test_request.py b/tests/test_request.py index ba24c9b..6d885d0 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,4 +1,4 @@ -"""Test for request module fuctionality.""" +"""Tests for request module functionality.""" from io import BytesIO from time import time from typing import Any, ClassVar @@ -24,8 +24,9 @@ def app(): class TestEmpty: - """Test for Empty class""" + """Tests for the Empty class.""" def test_emptry_form(self): + """Tests the EmptyForm class behavior.""" form = EmptyForm() assert form.getvalue("name") is None assert form.getvalue("name", "PooWSGI") == "PooWSGI" @@ -36,8 +37,9 @@ def test_emptry_form(self): class TestJSON: - """Test for JSON input class""" + """Tests for the JSON input classes.""" def test_json_dict(self): + """Tests the JsonDict class.""" json = JsonDict(age=23, items=[1, 2], size="25") assert json.getvalue("no") is None assert json.getvalue("name", "PooWSGI") == "PooWSGI" @@ -50,6 +52,7 @@ def test_json_dict(self): assert not tuple(json.getlist("values")) def test_json_list_empty(self): + """Tests the JsonList class with an empty list.""" json = JsonList() assert json.getvalue("no") is None assert json.getvalue("name", "PooWSGI") == "PooWSGI" @@ -61,6 +64,7 @@ def test_json_list_empty(self): assert not tuple(json.getlist("ages")) def test_json_list(self): + """Tests the JsonList class with a populated list.""" json = JsonList([1, 2]) assert json.getvalue("age") == 1 assert json.getfirst("age") == 1 @@ -68,14 +72,15 @@ def test_json_list(self): class TestArgs: - """Tests for Args class""" + """Tests for the Args class.""" class Req: - """Request class mock""" + """A mock Request class.""" app = None query = '' environ: ClassVar[dict[str, Any]] = {} def test_empty(self): + """Tests the Args class with empty arguments.""" args = Args(self.Req()) assert args.getvalue("no") is None assert args.getvalue("name", "PooWSGI") == "PooWSGI" @@ -87,7 +92,7 @@ def test_empty(self): class Empty: - """Empty Request class mock""" + """A mock Empty Request class.""" environ: ClassVar[dict[str, Any]] = {} headers: ClassVar[dict[str, str]] = {} input = BytesIO(b"") @@ -99,7 +104,7 @@ def empty(): class UrlEncoded: - """Request class with application/x-www-form-urlencoded content.""" + """A mock Request class with application/x-www-form-urlencoded content.""" environ: ClassVar[dict[str, Any]] = {} headers = Headers({ "Content-Length": "60", @@ -116,7 +121,7 @@ def url_encoded(): class MultiPart: - """Request class with multipart/form-data content.""" + """A mock Request class with multipart/form-data content.""" environ: ClassVar[dict[str, Any]] = {} headers = Headers({ "Content-Type": @@ -153,7 +158,7 @@ def multipart(): class TxtFile: - """Request class with bin file in multipart content.""" + """A mock Request class with a binary file in multipart content.""" environ: ClassVar[dict[str, Any]] = {} headers = Headers({ "Content-Length": "293", @@ -178,8 +183,9 @@ def txt_file(): class TestForm: - """Tests for FieldStorage""" + """Tests for FieldStorage.""" def test_empty(self, empty): + """Tests parsing an empty form.""" parser = FieldStorageParser(empty.input, empty.headers) form = parser.parse() @@ -192,6 +198,7 @@ def test_empty(self, empty): assert not tuple(form.getlist("values")) def test_multipart(self, multipart): + """Tests parsing a multipart form.""" parser = FieldStorageParser(multipart.input, multipart.headers) form = parser.parse() @@ -203,6 +210,7 @@ def test_multipart(self, multipart): assert form.getvalue("data") == "#"*2000 def test_urlencoded(self, url_encoded): + """Tests parsing a URL-encoded form.""" parser = FieldStorageParser(url_encoded.input, url_encoded.headers) form = parser.parse() @@ -212,6 +220,7 @@ def test_urlencoded(self, url_encoded): assert list(form.getlist("px", func=int)) == [8, 7, 6] def test_txtfile(self, txt_file): + """Tests parsing a text file in a form.""" parser = FieldStorageParser(txt_file.input, txt_file.headers) form = parser.parse() @@ -226,6 +235,7 @@ def test_txtfile(self, txt_file): assert isinstance(form.getvalue("file"), bytes) def test_txtfile_callback(self, txt_file): + """Tests parsing a text file using a file_callback.""" tmp = BytesIO() def file_callback(filename: str): @@ -246,52 +256,65 @@ def file_callback(filename: str): class TestParseJson: """Tests for parsing JSON requests.""" def test_str(self): + """Tests parsing a JSON string.""" assert isinstance(parse_json_request(b"{}"), JsonDict) def test_list(self): + """Tests parsing a JSON list.""" assert isinstance(parse_json_request(b"[]"), JsonList) def test_text(self): + """Tests parsing JSON plain text.""" assert isinstance(parse_json_request(b'"text"'), str) def test_int(self): + """Tests parsing a JSON integer.""" assert isinstance(parse_json_request(b"23"), int) def test_float(self): + """Tests parsing a JSON float.""" assert isinstance(parse_json_request(b"3.14"), float) def test_bool(self): + """Tests parsing a JSON boolean.""" assert isinstance(parse_json_request(b"true"), bool) def test_null(self): + """Tests parsing a JSON null value.""" assert parse_json_request(b"null") is None def test_error(self): + """Tests parsing an invalid JSON string, expecting an HTTPException.""" with raises(HTTPException) as err: parse_json_request(BytesIO(b"abraka")) assert err.value.args[0] == 400 assert 'error' in err.value.args[1] def test_unicode(self): + """Tests parsing JSON with Unicode characters.""" rval = parse_json_request(b'"\\u010de\\u0161tina"') assert rval == "čeština" def test_utf8(self): + """Tests parsing JSON with UTF-8 encoded characters.""" rval = parse_json_request(b'"\xc4\x8de\xc5\xa1tina"') assert rval == "čeština" def test_unicode_struct(self): + """Tests parsing a JSON structure with Unicode characters.""" rval = parse_json_request(b'{"lang":"\\u010de\\u0161tina"}') assert rval == {"lang": "čeština"} def test_utf_struct(self): + """Tests parsing a JSON structure with UTF-8 encoded characters.""" rval = parse_json_request(b'{"lang":"\xc4\x8de\xc5\xa1tina"}') assert rval == {"lang": "čeština"} class TestRequest: - """Test Request class.""" + """Tests the Request class.""" def test_host_wsgi(self, app): + """Tests Request host resolution with WSGI environment variables.""" env = { 'PATH_INFO': '/path', 'wsgi.url_scheme': 'http', @@ -306,6 +329,7 @@ def test_host_wsgi(self, app): assert req.construct_url('/x') == 'http://example.org/x' def test_host_header(self, app): + """Tests Request host resolution with HTTP_HOST header.""" env = { 'PATH_INFO': '/path', 'wsgi.url_scheme': 'http', @@ -321,6 +345,7 @@ def test_host_header(self, app): assert req.construct_url('/x') == 'http://example.net:8080/x' def test_forward_header(self, app): + """Tests Request host resolution with X-Forwarded headers.""" env = { 'PATH_INFO': '/path', 'wsgi.url_scheme': 'http', @@ -338,6 +363,7 @@ def test_forward_header(self, app): assert req.construct_url('/x') == 'https://example.com/x' def test_empty_form(self, app): + """Tests request handling with an empty form.""" env = { 'PATH_INFO': '/path', 'SERVER_PROTOCOL': 'HTTP/1.0', @@ -354,7 +380,7 @@ def test_empty_form(self, app): def test_bad_path_info_triggers_400(app): - """Test that bad PATH_INFO encoding is handled and returns 400.""" + """Tests that bad PATH_INFO encoding is handled and returns 400.""" captured_status = None captured_headers = None diff --git a/tests/test_responses.py b/tests/test_responses.py index 0c0b71b..1ef9e4e 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -1,16 +1,28 @@ -"""Test for Response objects and it's functionality.""" -from io import BufferedWriter, BytesIO -from datetime import datetime, timezone +"""Tests for Response objects and their functionality.""" -from simplejson import load, loads +from datetime import datetime, timezone +from io import BufferedWriter, BytesIO import pytest +from simplejson import load, loads -from poorwsgi.response import Response, JSONResponse, TextResponse, \ - GeneratorResponse, StrGeneratorResponse, JSONGeneratorResponse, \ - RedirectResponse, FileObjResponse, FileResponse, NotModifiedResponse, \ - PartialResponse, HTTPException, redirect, abort from poorwsgi.request import Headers +from poorwsgi.response import ( + FileObjResponse, + FileResponse, + GeneratorResponse, + HTTPException, + JSONGeneratorResponse, + JSONResponse, + NotModifiedResponse, + PartialResponse, + RedirectResponse, + Response, + StrGeneratorResponse, + TextResponse, + abort, + redirect, +) from poorwsgi.state import HTTP_NOT_FOUND # , HTTP_RANGE_NOT_SATISFIABLE # pylint: disable=missing-function-docstring @@ -25,7 +37,7 @@ (b"data", "application/octet-stream"), ("text", "text/plain", (("X-Header", "Value"),)), ("text", "text/plain", Headers((("X-Header", "Value"),))), - ("text", "text/plain", Headers((("X-Header", "Value"),)), HTTP_NOT_FOUND) + ("text", "text/plain", Headers((("X-Header", "Value"),)), HTTP_NOT_FOUND), ) kwargs = ( @@ -33,7 +45,7 @@ {"data": ""}, {"content_type": "text/plain"}, {"headers": (("X-Header", "Value"),)}, - {"status_code": HTTP_NOT_FOUND} + {"status_code": HTTP_NOT_FOUND}, ) @@ -59,245 +71,306 @@ def start_response(status_code, headers): class TestReponse: - """Basic tests for Response.""" + """Basic tests for Response objects.""" def test_args(self, response_args): + """Tests Response initialization with positional arguments.""" res = response_args(start_response) assert isinstance(res.read(), bytes) def test_kwargs(self, response_kwargs): + """Tests Response initialization with keyword arguments.""" res = response_kwargs(start_response) assert isinstance(res.read(), bytes) def test_once(self, response_args): + """Tests that a Response object can only be used once.""" response_args(start_response) with pytest.raises(RuntimeError): response_args(start_response) class TestPartial: - """Test for Partial Response""" + """Tests for Partial Response functionality.""" def test_no_accept_range(self): + """Tests that Accept-Ranges header is not set by default.""" res = Response() - assert res.headers.get('Accept-Ranges') is None + assert res.headers.get("Accept-Ranges") is None def test_make_partial(self): + """Tests the make_partial method with default units.""" res = Response() res.make_partial() - assert res.headers.get('Accept-Ranges') == 'bytes' + assert res.headers.get("Accept-Ranges") == "bytes" def test_make_partial_chunks(self): + """Tests the make_partial method with 'chunks' units.""" res = Response() res.make_partial(units="chunks") - assert res.headers.get('Accept-Ranges') == 'chunks' + assert res.headers.get("Accept-Ranges") == "chunks" def test_cant_be_partial(self): + """Tests that a non-HTTP_OK response cannot be partial.""" res = Response(status_code=HTTP_NOT_FOUND) res.make_partial() - assert res.headers.get('Accept-Ranges') is None + assert res.headers.get("Accept-Ranges") is None def test_cant_be_partial_after(self): + """Tests that partial content is cleared if status code changes after + make_partial.""" res = Response() res.make_partial([(0, 3)]) res.status_code = HTTP_NOT_FOUND - assert res.headers.get('Accept-Ranges') is None - assert not res.ranges + assert res.headers.get("Accept-Ranges") is None def test_partial_content_start(self): - res = Response(b'0123456789') + """Tests partial content response from the start of the content.""" + res = Response(b"0123456789") res.make_partial([(0, 4)]) - assert res(start_response).read() == b'01234' - assert int(res.headers.get('Content-Length')) == 5 - assert res.headers.get('Content-Range') == "bytes 0-4/10" + assert res(start_response).read() == b"01234" + assert int(res.headers.get("Content-Length")) == 5 + assert res.headers.get("Content-Range") == "bytes 0-4/10" def test_partial_content_mid(self): - res = Response(b'0123456789') + """Tests partial content response from the middle of the content.""" + res = Response(b"0123456789") res.make_partial([(3, 6)]) - assert res(start_response).read() == b'3456' - assert int(res.headers.get('Content-Length')) == 4 - assert res.headers.get('Content-Range') == "bytes 3-6/10" + assert res(start_response).read() == b"3456" + assert int(res.headers.get("Content-Length")) == 4 + assert res.headers.get("Content-Range") == "bytes 3-6/10" def test_partial_content_end(self): - res = Response(b'0123456789') + """Tests partial content response to the end of the content.""" + res = Response(b"0123456789") res.make_partial([(5, 9)]) - assert res(start_response).read() == b'56789' - assert int(res.headers.get('Content-Length')) == 5 - assert res.headers.get('Content-Range') == "bytes 5-9/10" + assert res(start_response).read() == b"56789" + assert int(res.headers.get("Content-Length")) == 5 + assert res.headers.get("Content-Range") == "bytes 5-9/10" def test_partial_content_more(self): - res = Response(b'0123456789') + """Tests partial content response with a range exceeding content + length.""" + res = Response(b"0123456789") res.make_partial([(8, 15)]) - assert res(start_response).read() == b'89' - assert int(res.headers.get('Content-Length')) == 2 - assert res.headers.get('Content-Range') == "bytes 8-9/10" + assert res(start_response).read() == b"89" + assert int(res.headers.get("Content-Length")) == 2 + assert res.headers.get("Content-Range") == "bytes 8-9/10" def test_partial_content_over(self): - res = Response(b'0123456789') + """Tests partial content response with a range entirely beyond content + length.""" + res = Response(b"0123456789") res.make_partial([(10, 15)]) with pytest.raises(HTTPException) as err: res(start_response) # assert isinstance(err.value.response, RangeNotSatisfiable) assert err.value.response.status_code == 416 - assert err.value.response.headers['Content-Range'] == "bytes 10-15/10" def test_partial_content_last(self): - res = Response(b'0123456789') + """Tests partial content response requesting the last N bytes.""" + res = Response(b"0123456789") res.make_partial([(None, 2)]) - assert res(start_response).read() == b'89' - assert int(res.headers.get('Content-Length')) == 2 - assert res.headers.get('Content-Range') == "bytes 8-9/10" + assert res(start_response).read() == b"89" + assert int(res.headers.get("Content-Length")) == 2 + assert res.headers.get("Content-Range") == "bytes 8-9/10" def test_partial_content_last_more(self): - res = Response(b'0123456789') + """Tests partial content response requesting more than available last N + bytes.""" + res = Response(b"0123456789") res.make_partial([(None, 20)]) - assert res(start_response).read() == b'0123456789' - assert int(res.headers.get('Content-Length')) == 10 - assert res.headers.get('Content-Range') == "bytes 0-9/10" + assert res(start_response).read() == b"0123456789" + assert int(res.headers.get("Content-Length")) == 10 + assert res.headers.get("Content-Range") == "bytes 0-9/10" def test_partial_content_from(self): - res = Response(b'0123456789') + """Tests partial content response from a specific byte to the end.""" + res = Response(b"0123456789") res.make_partial([(7, None)]) - assert res(start_response).read() == b'789' - assert int(res.headers.get('Content-Length')) == 3 - assert res.headers.get('Content-Range') == "bytes 7-9/10" + assert res(start_response).read() == b"789" + assert int(res.headers.get("Content-Length")) == 3 + assert res.headers.get("Content-Range") == "bytes 7-9/10" def test_partial_contents(self): - res = Response(b'0123456789') + """Tests partial content response with multiple ranges (only first is + used).""" + res = Response(b"0123456789") # Not supported now res.make_partial([(0, 2), (8, 9)]) # Only first range was returned - assert res(start_response).read() == b'012' - assert int(res.headers.get('Content-Length')) == 3 - assert res.headers.get('Content-Range') == "bytes 0-2/10" + assert res(start_response).read() == b"012" + assert int(res.headers.get("Content-Length")) == 3 + assert res.headers.get("Content-Range") == "bytes 0-2/10" def test_unknown_units(self): - res = Response(b'0123456789') + """Tests partial content response with unknown units, expecting full + response.""" + res = Response(b"0123456789") res.make_partial([(2, 4)], "lines") - assert res(start_response).read() == b'0123456789' + assert res(start_response).read() == b"0123456789" assert "Content-Range" not in res.headers assert res.headers.get("Accept-Ranges") == "lines" class TestPartialResponse: - """Test for special PartialResponse class.""" + """Tests for the special PartialResponse class.""" def test_response(self): - res = PartialResponse(b'56789') + """Tests basic PartialResponse with custom units.""" + res = PartialResponse(b"56789") res.make_range([(5, 9)], "chars") - assert res(start_response).read() == b'56789' - assert int(res.headers.get('Content-Length')) == 5 - assert res.headers.get('Content-Range') == "chars 5-9/*" + assert res(start_response).read() == b"56789" + assert int(res.headers.get("Content-Length")) == 5 + assert res.headers.get("Content-Range") == "chars 5-9/*" def test_full(self): - res = PartialResponse(b'56789') + """Tests PartialResponse with full range information.""" + res = PartialResponse(b"56789") res.make_range([(5, 9)], "chars", 25) - assert res(start_response).read() == b'56789' - assert int(res.headers.get('Content-Length')) == 5 - assert res.headers.get('Content-Range') == "chars 5-9/25" + assert res(start_response).read() == b"56789" + assert int(res.headers.get("Content-Length")) == 5 + assert res.headers.get("Content-Range") == "chars 5-9/25" def test_partial(self): - res = PartialResponse(b'56789') + """Tests make_partial method behavior in PartialResponse (should do + nothing).""" + res = PartialResponse(b"56789") res.make_partial([(5, 9)], "chars") assert "Accept-Range" not in res.headers assert "Content-Range" not in res.headers class TestPartialGenerator: - """Test for Partial Response via generators.""" + """Tests for Partial Response via generators.""" def test_partial_known_length(self): - res = GeneratorResponse((str(x).encode("utf-8") for x in range(10)), - content_length=10) + """Tests partial response for generators with known content length from + a start byte.""" + res = GeneratorResponse( + (str(x).encode("utf-8") for x in range(10)), content_length=10 + ) res.make_partial([(7, None)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 3 - assert res.headers.get('Content-Range') == "bytes 7-9/10" + assert int(res.headers.get("Content-Length")) == 3 + assert res.headers.get("Content-Range") == "bytes 7-9/10" assert b"".join(gen) == b"789" def test_partial_known_length_rewrite(self): - res = GeneratorResponse((str(x).encode("utf-8") for x in range(10)), - content_length=10, - headers={"Content-Length": "10"}) + """Tests partial response for generators with known content length and + Content-Length header rewritten.""" + res = GeneratorResponse( + (str(x).encode("utf-8") for x in range(10)), + content_length=10, + headers={"Content-Length": "10"}, + ) res.make_partial([(7, None)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 3 - assert res.headers.get('Content-Range') == "bytes 7-9/10" + assert int(res.headers.get("Content-Length")) == 3 + assert res.headers.get("Content-Range") == "bytes 7-9/10" assert b"".join(gen) == b"789" def test_partial_known_length_blocks_start(self): - res = GeneratorResponse((str(x).encode("utf-8")*3 for x in range(10)), - content_length=30) + """Tests partial response for generators with known content length and + block reading from start.""" + res = GeneratorResponse( + (str(x).encode("utf-8") * 3 for x in range(10)), content_length=30 + ) res.make_partial([(0, 6)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 7 - assert res.headers.get('Content-Range') == "bytes 0-6/30" + assert int(res.headers.get("Content-Length")) == 7 + assert res.headers.get("Content-Range") == "bytes 0-6/30" assert b"".join(gen) == b"0001112" def test_partial_known_length_blocks_start_rewrite(self): - res = GeneratorResponse((str(x).encode("utf-8")*3 for x in range(10)), - content_length=30, - headers={"Content-Length": "30"}) + """Tests partial response for generators with known content length, + block reading from start, and Content-Length header rewritten.""" + res = GeneratorResponse( + (str(x).encode("utf-8") * 3 for x in range(10)), + content_length=30, + headers={"Content-Length": "30"}, + ) res.make_partial([(0, 6)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 7 - assert res.headers.get('Content-Range') == "bytes 0-6/30" + assert int(res.headers.get("Content-Length")) == 7 + assert res.headers.get("Content-Range") == "bytes 0-6/30" assert b"".join(gen) == b"0001112" def test_partial_known_length_blocks_range(self): - res = GeneratorResponse((str(x).encode("utf-8")*3 for x in range(10)), - content_length=30) + """Tests partial response for generators with known content length and + block reading within a range.""" + res = GeneratorResponse( + (str(x).encode("utf-8") * 3 for x in range(10)), content_length=30 + ) res.make_partial([(8, 16)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 9 - assert res.headers.get('Content-Range') == "bytes 8-16/30" + assert int(res.headers.get("Content-Length")) == 9 + assert res.headers.get("Content-Range") == "bytes 8-16/30" assert b"".join(gen) == b"233344455" def test_partial_known_length_blocks_range_rewrite(self): - res = GeneratorResponse((str(x).encode("utf-8")*3 for x in range(10)), - content_length=30, - headers={"Content-Length": "30"}) + """Tests partial response for generators with known content length, + block reading within a range, and Content-Length header rewritten.""" + res = GeneratorResponse( + (str(x).encode("utf-8") * 3 for x in range(10)), + content_length=30, + headers={"Content-Length": "30"}, + ) res.make_partial([(8, 16)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 9 - assert res.headers.get('Content-Range') == "bytes 8-16/30" + assert int(res.headers.get("Content-Length")) == 9 + assert res.headers.get("Content-Range") == "bytes 8-16/30" assert b"".join(gen) == b"233344455" def test_partial_known_length_blocks_range2(self): - res = GeneratorResponse((b'01234' for x in range(5)), - content_length=25) + """Tests partial response for generators with known content length and + block reading within a specific small range.""" + res = GeneratorResponse( + (b"01234" for x in range(5)), content_length=25 + ) res.make_partial([(7, 8)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 2 - assert res.headers.get('Content-Range') == "bytes 7-8/25" + assert int(res.headers.get("Content-Length")) == 2 + assert res.headers.get("Content-Range") == "bytes 7-8/25" assert b"".join(gen) == b"23" def test_partial_known_length_blocks_end(self): - res = GeneratorResponse((str(x).encode("utf-8")*3 for x in range(10)), - content_length=30) + """Tests partial response for generators with known content length and + block reading from the end.""" + res = GeneratorResponse( + (str(x).encode("utf-8") * 3 for x in range(10)), content_length=30 + ) res.make_partial([(None, 7)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 7 - assert res.headers.get('Content-Range') == "bytes 23-29/30" + assert int(res.headers.get("Content-Length")) == 7 + assert res.headers.get("Content-Range") == "bytes 23-29/30" assert b"".join(gen) == b"7888999" def test_partial_known_length_blocks_end_rewrite(self): - res = GeneratorResponse((str(x).encode("utf-8")*3 for x in range(10)), - content_length=30, - headers={"Content-Length": "30"}) + """Tests partial response for generators with known content length, + block reading from the end, and Content-Length header rewritten.""" + res = GeneratorResponse( + (str(x).encode("utf-8") * 3 for x in range(10)), + content_length=30, + headers={"Content-Length": "30"}, + ) res.make_partial([(None, 7)]) gen = res(start_response) - assert int(res.headers.get('Content-Length')) == 7 - assert res.headers.get('Content-Range') == "bytes 23-29/30" + assert int(res.headers.get("Content-Length")) == 7 + assert res.headers.get("Content-Range") == "bytes 23-29/30" assert b"".join(gen) == b"7888999" def test_partial_unknown_length_start(self): + """Tests partial response for generators with unknown content length + from a start byte, expecting HTTPException.""" res = GeneratorResponse((str(x).encode("utf-8") for x in range(10))) res.make_partial([(7, None)]) with pytest.raises(HTTPException): res(start_response) def test_partial_unknown_length_range(self): + """Tests partial response for generators with unknown content length + within a range, expecting HTTPException.""" res = GeneratorResponse((str(x).encode("utf-8") for x in range(10))) res.make_partial([(7, 9)]) with pytest.raises(HTTPException): @@ -305,6 +378,8 @@ def test_partial_unknown_length_range(self): # assert err.status_code == HTTP_RANGE_NOT_SATISFIABLE def test_partial_unknown_length_end(self): + """Tests partial response for generators with unknown content length + from the end, expecting HTTPException.""" res = GeneratorResponse((str(x).encode("utf-8") for x in range(10))) res.make_partial([(None, 7)]) with pytest.raises(HTTPException): @@ -316,93 +391,112 @@ class TestJSONResponse: """Tests for JSONResponse.""" def test_kwargs(self): + """Tests JSONResponse initialization with keyword arguments.""" res = JSONResponse(items=list(range(5))) data = load(res(start_response)) assert data == {"items": [0, 1, 2, 3, 4]} assert res.content_length == 26 def test_charset(self): + """Tests JSONResponse with a specified charset.""" res = JSONResponse(msg="Message") res(start_response) - assert res.headers['Content-Type'] == "application/json; charset=utf-8" + assert res.headers["Content-Type"] == "application/json; charset=utf-8" def test_content_length(self): + """Tests Content-Length header in JSONResponse.""" res = JSONResponse(msg="Message") res(start_response) - assert int(res.headers.get('Content-Length')) == 18 + assert int(res.headers.get("Content-Length")) == 18 def test_no_charset(self): + """Tests JSONResponse when no charset is specified.""" res = JSONResponse(msg="Message", charset=None) res(start_response) - assert res.headers.get('Content-Type') == "application/json" + assert res.headers.get("Content-Type") == "application/json" def test_once(self): + """Tests that a JSONResponse object can only be used once.""" response = JSONResponse(msg="Message", charset=None) response(start_response) with pytest.raises(RuntimeError): response(start_response) def test_null(self): + """Tests JSONResponse with null data.""" response = JSONResponse() data = load(response(start_response)) assert data is None def test_list_of_objects(self): - response = JSONResponse([{'x': 1}, {'x': 2}]) + """Tests JSONResponse with a list of objects.""" + response = JSONResponse([{"x": 1}, {"x": 2}]) data = load(response(start_response)) - assert data == [{'x': 1}, {'x': 2}] + assert data == [{"x": 1}, {"x": 2}] def test_partial_content_start(self): - response = JSONResponse([{'x': 1}, {'x': 2}]) + """Tests partial content response for JSONResponse from the start.""" + response = JSONResponse([{"x": 1}, {"x": 2}]) response.make_partial([(0, 4)]) assert response(start_response).read() == b'[{"x"' def test_data_or_kwargs(self): + """Tests that JSONResponse raises an error if both data_ and kwargs are + provided.""" with pytest.raises(RuntimeError): JSONResponse([], msg="Messgae") def test_no_decoded_unicode(self): - res = JSONResponse(msg="Čeština", - encoder_kwargs={"ensure_ascii": False}) + """Tests JSONResponse with non-ASCII characters and + ensure_ascii=False.""" + res = JSONResponse( + msg="Čeština", encoder_kwargs={"ensure_ascii": False} + ) data = res.data assert data == b'{"msg": "\xc4\x8ce\xc5\xa1tina"}' class TestTextResponse: - """Test for TextResponse.""" + """Tests for TextResponse.""" def test_simple(self): + """Tests a simple TextResponse.""" res = TextResponse("Simple text") res.content_type = "text/plain; charset=utf-8" assert res.data == b"Simple text" assert res.content_length == 11 def test_no_charset(self): + """Tests TextResponse when no charset is specified.""" res = TextResponse("Simple text", charset=None) res.content_type = "text/plain" assert res.data == b"Simple text" assert res.content_length == 11 def test_content_length(self): + """Tests Content-Length header in TextResponse.""" res = TextResponse("Simple text") res(start_response) - assert int(res.headers.get('Content-Length')) == 11 + assert int(res.headers.get("Content-Length")) == 11 class TestGeneratorResponse: """Tests for GeneratorResponse classes.""" def test_generator(self): + """Tests a basic GeneratorResponse.""" res = GeneratorResponse((str(x).encode("utf-8") for x in range(5))) gen = res(start_response) assert b"".join(gen) == b"01234" def test_str_generator(self): + """Tests a StrGeneratorResponse.""" res = StrGeneratorResponse((str(x) for x in range(5))) gen = res(start_response) assert b"".join(gen) == b"01234" def test_once(self): + """Tests that a GeneratorResponse object can only be used once.""" response = StrGeneratorResponse((str(x) for x in range(5))) response(start_response) with pytest.raises(RuntimeError): @@ -410,25 +504,29 @@ def test_once(self): class TestJSONGenerarorResponse: - """Test. for JSONGeneratorResponse.""" + """Tests for JSONGeneratorResponse.""" def test_generator(self): + """Tests a JSONGeneratorResponse.""" res = JSONGeneratorResponse(items=range(5)) gen = res(start_response) data = loads(b"".join(gen)) assert data == {"items": [0, 1, 2, 3, 4]} def test_charset(self): + """Tests JSONGeneratorResponse with a specified charset.""" res = JSONGeneratorResponse(items=range(5)) res(start_response) - assert res.headers['Content-Type'] == "application/json; charset=utf-8" + assert res.headers["Content-Type"] == "application/json; charset=utf-8" def test_no_charset(self): + """Tests JSONGeneratorResponse when no charset is specified.""" res = JSONGeneratorResponse(items=range(5), charset=None) res(start_response) - assert res.headers.get('Content-Type') == "application/json" + assert res.headers.get("Content-Type") == "application/json" def test_once(self): + """Tests that a JSONGeneratorResponse object can only be used once.""" response = JSONGeneratorResponse(items=range(5), charset=None) response(start_response) with pytest.raises(RuntimeError): @@ -436,60 +534,67 @@ def test_once(self): class TestRedirectResponse: - """Test for RedirectResponse and redirect function.""" + """Tests for RedirectResponse and the redirect function.""" def test_init(self): - res = RedirectResponse('/', 303, message='See Other') + """Tests RedirectResponse initialization.""" + res = RedirectResponse("/", 303, message="See Other") assert res.status_code == 303 - assert res.data == b'See Other' - assert res.headers['Location'] == '/' + assert res.data == b"See Other" + assert res.headers["Location"] == "/" def test_init_deprecated(self): - res = RedirectResponse('/true', True) + """Tests RedirectResponse initialization with deprecated arguments.""" + res = RedirectResponse("/true", True) assert res.status_code == 301 - assert res.headers['Location'] == '/true' + assert res.headers["Location"] == "/true" - res = RedirectResponse('/permanent', permanent=True) + res = RedirectResponse("/permanent", permanent=True) assert res.status_code == 301 - assert res.headers['Location'] == '/permanent' + assert res.headers["Location"] == "/permanent" def test_redirect(self): + """Tests the redirect function with default arguments.""" with pytest.raises(HTTPException) as err: - redirect('/') + redirect("/") assert isinstance(err.value.response, RedirectResponse) assert err.value.response.status_code == 302 def test_redirect_deprecated(self): + """Tests the redirect function with deprecated arguments.""" with pytest.raises(HTTPException) as err: - redirect('/', True) + redirect("/", True) assert isinstance(err.value.response, RedirectResponse) assert err.value.response.status_code == 301 with pytest.raises(HTTPException) as err: - redirect('/', permanent=True) + redirect("/", permanent=True) assert isinstance(err.value.response, RedirectResponse) assert err.value.response.status_code == 301 class TestHTTPException: - """Tests for HTTPException and other functions which raise that.""" + """Tests for HTTPException and other functions that raise it.""" def test_redirect(self): + """Tests HTTPException raised by redirect.""" with pytest.raises(HTTPException) as err: - redirect('/') + redirect("/") assert err.value.status_code == 302 def test_abort_status_code(self): + """Tests abort with a status code.""" with pytest.raises(HTTPException) as err: abort(404) assert err.value.status_code == 404 def test_abort_response(self): + """Tests abort with a Response object.""" with pytest.raises(HTTPException) as err: abort(Response(status_code=400)) @@ -497,77 +602,96 @@ def test_abort_response(self): assert err.value.status_code == 400 def test_ordinary_exception(self): + """Tests raising a simple HTTPException.""" with pytest.raises(HTTPException) as err: raise HTTPException(500) assert err.value.status_code == 500 -class TestFileResponse(): +class TestFileResponse: """Tests for file type responses.""" def test_assert_readable(self): + """Tests that FileObjResponse asserts if the file object is not + readable.""" with pytest.raises(AssertionError): FileObjResponse(BufferedWriter(BytesIO())) def test_assert_text(self): + """Tests that FileObjResponse asserts if the file object is a text + stream.""" with pytest.raises(AssertionError): - with open(__file__, 'rt', encoding='utf-8') as file_: + with open(__file__, "rt", encoding="utf-8") as file_: FileObjResponse(file_) def test_last_modified_header(self): + """Tests that FileResponse sets the Last-Modified header.""" res = FileResponse(__file__) - assert res.headers.get('Last-Modified') is not None + assert res.headers.get("Last-Modified") is not None def test_accept_range(self): + """Tests that FileResponse sets the Accept-Ranges header.""" res = FileResponse(__file__) - assert res.headers.get('Accept-Ranges') == 'bytes' + assert res.headers.get("Accept-Ranges") == "bytes" def test_partial_content_start(self): + """Tests partial content response for FileResponse from the start.""" res = FileResponse(__file__) res.make_partial([(0, 4)]) assert res(start_response).read() == b'"""Te' - assert int(res.headers.get('Content-Length')) == 5 + assert int(res.headers.get("Content-Length")) == 5 def test_partial_content_mid(self): + """Tests partial content response for FileResponse from the middle.""" res = FileResponse(__file__) res.make_partial([(3, 6)]) - assert res(start_response).read() == b'Test' - assert int(res.headers.get('Content-Length')) == 4 + assert res(start_response).read() == b"Test" + assert int(res.headers.get("Content-Length")) == 4 def test_partial_content_last(self): + """Tests partial content response for FileResponse requesting the last + N bytes.""" res = FileResponse(__file__) res.make_partial([(None, 4)]) - assert res(start_response).read() == b'one\n' - assert int(res.headers.get('Content-Length')) == 4 + assert res(start_response).read() == b"one\n" + assert int(res.headers.get("Content-Length")) == 4 -class TestNotModifiedResponse(): +class TestNotModifiedResponse: """Tests for NotModifiedResponse.""" def test_params(self): - res = NotModifiedResponse(etag='W/"etag"', - content_location="content-location", - date="22 Apr 2022", - vary="yrav") - assert res.headers.get('ETag') == 'W/"etag"' - assert res.headers.get('Content-Location') == "content-location" - assert res.headers.get('Date') == "22 Apr 2022" - assert res.headers.get('Vary') == "yrav" + """Tests NotModifiedResponse initialization with various parameters.""" + res = NotModifiedResponse( + etag='W/"etag"', + content_location="content-location", + date="22 Apr 2022", + vary="yrav", + ) + assert res.headers.get("ETag") == 'W/"etag"' + assert res.headers.get("Content-Location") == "content-location" + assert res.headers.get("Date") == "22 Apr 2022" + assert res.headers.get("Vary") == "yrav" def test_date_time(self): + """Tests NotModifiedResponse with a timestamp for the Date header.""" res = NotModifiedResponse(date=0) - assert res.headers.get('Date') == "Thu, 01 Jan 1970 00:00:00 GMT" + assert res.headers.get("Date") == "Thu, 01 Jan 1970 00:00:00 GMT" def test_date_datetime(self): - res = NotModifiedResponse( - date=datetime.fromtimestamp(0, timezone.utc)) - assert res.headers.get('Date') == "Thu, 01 Jan 1970 00:00:00 GMT" + """Tests NotModifiedResponse with a datetime object for the Date + header.""" + res = NotModifiedResponse(date=datetime.fromtimestamp(0, timezone.utc)) + assert res.headers.get("Date") == "Thu, 01 Jan 1970 00:00:00 GMT" def test_etag_only(self): + """Tests NotModifiedResponse with only ETag specified.""" res = NotModifiedResponse(etag='W/"cd04a47544"') assert res.headers.get("ETag") == 'W/"cd04a47544"' def test_date_empty_string(self): + """Tests NotModifiedResponse when an empty string is provided for the + Date header.""" res = NotModifiedResponse(date="") - assert res.headers.get('Date') is None + assert res.headers.get("Date") is None diff --git a/tests/test_route_validation.py b/tests/test_route_validation.py index 56ddf1d..656a3da 100644 --- a/tests/test_route_validation.py +++ b/tests/test_route_validation.py @@ -1,215 +1,228 @@ """Unit tests for route filter validation.""" + import pytest -from poorwsgi.wsgi import Application from poorwsgi.state import METHOD_GET +from poorwsgi.wsgi import Application def test_valid_route_no_filter(): - """Test that routes without filters work correctly.""" - app = Application('test_valid_no_filter') + """Tests that routes without filters work correctly.""" + app = Application("test_valid_no_filter") - @app.route('/api/users') - def handler(req): - return 'ok' + @app.route("/api/users") + def handler(_req): + return "ok" - assert '/api/users' in app.routes + assert "/api/users" in app.routes def test_valid_route_with_int_filter(): - """Test that routes with :int filter work correctly.""" - app = Application('test_valid_int') + """Tests that routes with :int filters work correctly.""" + app = Application("test_valid_int") - @app.route('/api/users/') - def handler(req, id): # noqa: A002 - return 'ok' + @app.route("/api/users/") + def handler(_req, _id): + return "ok" # Route with filter should be in regular_routes assert len(app.regular_routes) > 0 def test_valid_route_with_word_filter(): - """Test that routes with :word filter work correctly.""" - app = Application('test_valid_word') + """Tests that routes with :word filters work correctly.""" + app = Application("test_valid_word") - @app.route('/api/users/') - def handler(req, name): - return 'ok' + @app.route("/api/users/") + def handler(_req, _name): + return "ok" assert len(app.regular_routes) > 0 def test_valid_route_with_float_filter(): - """Test that routes with :float filter work correctly.""" - app = Application('test_valid_float') + """Tests that routes with :float filters work correctly.""" + app = Application("test_valid_float") - @app.route('/api/values/') - def handler(req, value): - return 'ok' + @app.route("/api/values/") + def handler(_req, _value): + return "ok" assert len(app.regular_routes) > 0 def test_valid_route_with_uuid_filter(): - """Test that routes with :uuid filter work correctly.""" - app = Application('test_valid_uuid') + """Tests that routes with :uuid filters work correctly.""" + app = Application("test_valid_uuid") - @app.route('/api/objects/') - def handler(req, id): # noqa: A002 - return 'ok' + @app.route("/api/objects/") + def handler(_req, _id): + return "ok" assert len(app.regular_routes) > 0 def test_valid_route_with_hex_filter(): - """Test that routes with :hex filter work correctly.""" - app = Application('test_valid_hex') + """Tests that routes with :hex filters work correctly.""" + app = Application("test_valid_hex") - @app.route('/api/codes/') - def handler(req, code): - return 'ok' + @app.route("/api/codes/") + def handler(_req, _code): + return "ok" assert len(app.regular_routes) > 0 def test_valid_route_with_multiple_filters(): - """Test that routes with multiple filters work correctly.""" - app = Application('test_valid_multiple') + """Tests that routes with multiple filters work correctly.""" + app = Application("test_valid_multiple") - @app.route('/api//') - def handler(req, entity, id): # noqa: A002 - return 'ok' + @app.route("/api//") + def handler(_req, _entity, _id): + return "ok" assert len(app.regular_routes) > 0 def test_invalid_route_space_after_open_bracket(): - """Test that space after < is rejected.""" - app = Application('test_invalid_space_after_open') + """Tests that a space after < is rejected.""" + app = Application("test_invalid_space_after_open") - with pytest.raises(ValueError, match=r'Invalid route definition.*must not contain spaces'): - @app.route('/api/< id:int>') - def handler(req, id): # noqa: A002 - return 'ok' + with pytest.raises( + ValueError, match=r"Invalid route definition.*must not contain spaces" + ): + + @app.route("/api/< id:int>") + def handler(_req, _id): + return "ok" def test_invalid_route_space_before_close_bracket(): - """Test that space before > is rejected.""" - app = Application('test_invalid_space_before_close') + """Tests that a space before > is rejected.""" + app = Application("test_invalid_space_before_close") + + with pytest.raises(ValueError, match="Invalid route definition"): - with pytest.raises(ValueError, match='Invalid route definition'): - @app.route('/api/') - def handler(req, id): # noqa: A002 - return 'ok' + @app.route("/api/") + def handler(_req, _id): + return "ok" def test_invalid_route_space_after_name(): - """Test that space after parameter name is rejected.""" - app = Application('test_invalid_space_after_name') + """Tests that a space after the parameter name is rejected.""" + app = Application("test_invalid_space_after_name") + + with pytest.raises(ValueError, match="Invalid route definition"): - with pytest.raises(ValueError, match='Invalid route definition'): - @app.route('/api/') - def handler(req, id): # noqa: A002 - return 'ok' + @app.route("/api/") + def handler(_req, _id): + return "ok" def test_invalid_route_space_after_colon(): - """Test that space after : is rejected.""" - app = Application('test_invalid_space_after_colon') + """Tests that a space after : is rejected.""" + app = Application("test_invalid_space_after_colon") - with pytest.raises(ValueError, match='Invalid route definition'): - @app.route('/api/') - def handler(req, id): # noqa: A002 - return 'ok' + with pytest.raises(ValueError, match="Invalid route definition"): + + @app.route("/api/") + def handler(_req, _id): + return "ok" def test_invalid_route_multiple_spaces(): - """Test that multiple spaces are rejected.""" - app = Application('test_invalid_multiple_spaces') + """Tests that multiple spaces are rejected.""" + app = Application("test_invalid_multiple_spaces") + + with pytest.raises(ValueError, match="Invalid route definition"): - with pytest.raises(ValueError, match='Invalid route definition'): - @app.route('/api/< id : int >') - def handler(req, id): # noqa: A002 - return 'ok' + @app.route("/api/< id : int >") + def handler(_req, _id): + return "ok" def test_error_message_provides_examples(): - """Test that error message includes helpful examples.""" - app = Application('test_error_message') + """Tests that the error message includes helpful examples.""" + app = Application("test_error_message") + + with pytest.raises( + ValueError, match="Invalid route definition" + ) as exc_info: - with pytest.raises(ValueError, match='Invalid route definition') as exc_info: - @app.route('/api/') - def handler(req, id): # noqa: A002 - return 'ok' + @app.route("/api/") + def handler(_req, _id): + return "ok" error_message = str(exc_info.value) - assert '' in error_message - assert '' in error_message - assert '' in error_message + assert "" in error_message + assert "" in error_message + assert "" in error_message def test_set_route_with_space_rejection(): - """Test that set_route method also rejects spaces.""" - app = Application('test_set_route_space') + """Tests that the set_route method also rejects spaces.""" + app = Application("test_set_route_space") - def handler(req, id): # noqa: A002 - return 'ok' + def handler(_req, _id): + return "ok" - with pytest.raises(ValueError, match='Invalid route definition'): - app.set_route('/api/< id:int>', handler, METHOD_GET) + with pytest.raises(ValueError, match="Invalid route definition"): + app.set_route("/api/< id:int>", handler, METHOD_GET) def test_route_with_no_filter_and_angle_brackets(): - """Test that routes without filters but with <> in path work.""" - app = Application('test_no_filter') + """Tests that routes without filters but with <> in the path work.""" + app = Application("test_no_filter") - @app.route('/api/') - def handler(req, id): # noqa: A002 - return 'ok' + @app.route("/api/") + def handler(_req, _id): + return "ok" # Route with but no filter should work assert len(app.regular_routes) > 0 def test_custom_filter_no_spaces(): - """Test that custom filters work without spaces.""" - app = Application('test_custom_filter') - app.set_filter('custom', r'[a-z]+') + """Tests that custom filters work without spaces.""" + app = Application("test_custom_filter") + app.set_filter("custom", r"[a-z]+") - @app.route('/api/') - def handler(req, value): - return 'ok' + @app.route("/api/") + def handler(_req, _value): + return "ok" assert len(app.regular_routes) > 0 def test_custom_filter_with_space_rejection(): - """Test that custom filters reject spaces.""" - app = Application('test_custom_filter_space') - app.set_filter('custom', r'[a-z]+') + """Tests that custom filters reject spaces.""" + app = Application("test_custom_filter_space") + app.set_filter("custom", r"[a-z]+") - with pytest.raises(ValueError, match='Invalid route definition'): - @app.route('/api/') - def handler(req, value): - return 'ok' + with pytest.raises(ValueError, match="Invalid route definition"): + + @app.route("/api/") + def handler(_req, _value): + return "ok" def test_re_filter_no_spaces(): - """Test that :re: filter works without spaces.""" - app = Application('test_re_filter') + """Tests that the :re: filter works without spaces.""" + app = Application("test_re_filter") - @app.route('/api/') - def handler(req, value): - return 'ok' + @app.route("/api/") + def handler(_req, _value): + return "ok" assert len(app.regular_routes) > 0 def test_re_filter_with_space_rejection(): - """Test that :re: filter rejects spaces before >.""" - app = Application('test_re_filter_space') + """Tests that the :re: filter rejects spaces before >.""" + app = Application("test_re_filter_space") + + with pytest.raises(ValueError, match="Invalid route definition"): - with pytest.raises(ValueError, match='Invalid route definition'): - @app.route('/api/') - def handler(req, value): - return 'ok' + @app.route("/api/") + def handler(_req, _value): + return "ok" diff --git a/tests/test_session.py b/tests/test_session.py index 07f79fa..43df035 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,7 +1,7 @@ -"""Unit tests for PoorSession class.""" +"""Unit tests for the PoorSession class.""" from os import urandom from sys import version_info -from http.cookies import SimpleCookie +from http.cookies import SimpleCookie, Morsel from typing import Any from pytest import fixture, raises, mark @@ -17,25 +17,25 @@ class Request: - """Request mock""" + """A mock Request class.""" secret_key = SECRET_KEY cookies: Any = SimpleCookie() class Empty: - """Request mock without secret key.""" + """A mock Request class without a secret key.""" secret_key = None @fixture def req(): - """Instance of Request object.""" + """An instance of a Request object.""" return Request() @fixture def req_session(): - """Instace of Request object with session cookie.""" + """An instance of a Request object with a session cookie.""" request = Request() session = PoorSession(request.secret_key) session.data['test'] = True @@ -45,9 +45,10 @@ def req_session(): class TestSession: - """Test PoorSession configuration options.""" + """Tests PoorSession configuration options.""" def test_default(self): + """Tests the default PoorSession configuration.""" session = PoorSession(SECRET_KEY) headers = session.header() assert "Expires" not in headers[0][1] @@ -56,42 +57,50 @@ def test_default(self): assert "Domain" not in headers[0][1] def test_destroy(self): + """Tests the destroy method of PoorSession.""" session = PoorSession(SECRET_KEY) session.destroy() headers = session.header() assert "; expires=" in headers[0][1] def test_expires(self): + """Tests PoorSession with an expires setting.""" session = PoorSession(SECRET_KEY, expires=10) headers = session.header() assert "; expires=" in headers[0][1] def test_max_age(self): + """Tests PoorSession with a max_age setting.""" session = PoorSession(SECRET_KEY, max_age=10) headers = session.header() assert "; Max-Age=10;" in headers[0][1] def test_no_path(self): + """Tests PoorSession when no path is specified.""" session = PoorSession(SECRET_KEY, path=None) headers = session.header() assert "Path" not in headers[0][1] def test_domain(self): + """Tests PoorSession with a specified domain.""" session = PoorSession(SECRET_KEY, domain="example.org") headers = session.header() assert "; Domain=example.org; " in headers[0][1] def test_httponly(self): + """Tests PoorSession with HttpOnly attribute.""" session = PoorSession(SECRET_KEY) headers = session.header() assert "; HttpOnly; " in headers[0][1] def test_http(self): + """Tests PoorSession without a secure setting (HTTP).""" session = PoorSession(SECRET_KEY) headers = session.header() assert "; Secure" not in headers[0][1] def test_https(self): + """Tests PoorSession with a secure setting (HTTPS).""" session = PoorSession(SECRET_KEY, secure=True) headers = session.header() assert "; Secure" in headers[0][1] @@ -100,65 +109,88 @@ def test_https(self): @mark.skipif(version_info.minor < 8, reason="SameSite is supported from Python 3.8") class TestSameSite: - """Test for PoorSession same_site option.""" + """Tests for the PoorSession same_site option.""" def test_default(self): + """Tests the default SameSite behavior of PoorSession.""" session = PoorSession(SECRET_KEY) headers = session.header() assert "; SameSite" not in headers[0][1] def test_none(self): + """Tests PoorSession with SameSite set to 'None'.""" session = PoorSession(SECRET_KEY, same_site="None") headers = session.header() assert "; SameSite=None" in headers[0][1] def test_lax(self): + """Tests PoorSession with SameSite set to 'Lax'.""" session = PoorSession(SECRET_KEY, same_site="Lax") headers = session.header() assert "; SameSite=Lax" in headers[0][1] def test_strict(self): + """Tests PoorSession with SameSite set to 'Strict'.""" session = PoorSession(SECRET_KEY, same_site="Strict") headers = session.header() assert "; SameSite=Strict" in headers[0][1] class TestErrors: - """Test exceptions""" + """Tests exceptions.""" def test_no_secret_key(self): + """Tests PoorSession initialization without a secret key, expecting + SessionError.""" with raises(SessionError): PoorSession(Empty) def test_bad_session(self): + """Tests loading a bad session cookie, expecting SessionError.""" + # pylint: disable=protected-access cookies = SimpleCookie() - cookies["SESSID"] = "\0" + morsel = Morsel() + morsel._key = 'SESSID' + morsel._value = '\0' + morsel._coded_value = '"\\000"' + cookies['SESSID'] = morsel session = PoorSession(SECRET_KEY) with raises(SessionError): session.load(cookies) def test_bad_session_compatibility(self, req): + """Tests PoorSession compatibility with a bad session cookie, expecting + SessionError.""" + # pylint: disable=protected-access req.cookies = SimpleCookie() - req.cookies["SESSID"] = "\0" + morsel = Morsel() + morsel._key = 'SESSID' + morsel._value = '\0' + morsel._coded_value = '"\\000"' + req.cookies['SESSID'] = morsel with raises(SessionError): PoorSession(req) class TestLoadWrite: - """Tests of load and write methods.""" + """Tests the load and write methods.""" def test_compatibility_empty(self, req): + """Tests compatibility with an empty request in PoorSession + constructor.""" session = PoorSession(req) assert session.data == {} def test_compatibility(self, req_session): + """Tests compatibility with a session cookie in PoorSession + constructor.""" session = PoorSession(req_session) assert session.data == {'test': True} def test_write_load(self, req_session): - """Method write was called in fixture req_session.""" + """Tests the write and load methods of PoorSession.""" session = PoorSession(SECRET_KEY) session.load(req_session.cookies) assert session.data == {'test': True} diff --git a/tests_integrity/conftest.py b/tests_integrity/conftest.py index 1185a99..8d665fc 100644 --- a/tests_integrity/conftest.py +++ b/tests_integrity/conftest.py @@ -1,8 +1,8 @@ -"""pytest configuration""" +"""Pytest configuration.""" def pytest_addoption(parser): - """Append new options for py.test command tool.""" + """Appends new options for the pytest command-line tool.""" parser.addoption( "--with-uwsgi", action="store_true", help="Run http server on uwsgi instead of internal server.") diff --git a/tests_integrity/openapi.py b/tests_integrity/openapi.py index a0f27bc..64d8bf5 100644 --- a/tests_integrity/openapi.py +++ b/tests_integrity/openapi.py @@ -1,16 +1,16 @@ -"""requests library object Wrapper for openapi_core library objects.""" -from urllib.parse import urlparse, parse_qs -from email.message import Message +"""Requests library object wrapper for openapi_core library objects.""" import json +from email.message import Message +from urllib.parse import parse_qs, urlparse from openapi_core import Spec # type: ignore from openapi_core.validation.request.datatypes import ( # type: ignore - RequestParameters) + RequestParameters) -class OpenAPIRequest(): - """requests.Request wrapper for openapi_core.""" +class OpenAPIRequest: + """A requests.Request wrapper for openapi_core.""" def __init__(self, request, path_pattern=None): self.full_url_pattern = path_pattern or request.url @@ -19,14 +19,16 @@ def __init__(self, request, path_pattern=None): self._url = urlparse(request.url) query = parse_qs(self._url.query) if self._url.query else {} # when args have one value, that is the value - args = tuple((key, val[0] if len(val) < 2 else val) - for key, val in query.items()) + args = tuple( + (key, val[0] if len(val) < 2 else val) + for key, val in query.items() + ) self.request = request self.data = request.data msg = Message() - msg['content-type'] = request.headers.get('Content-Type', '') + msg["content-type"] = request.headers.get("Content-Type", "") self.mimetype = msg.get_content_type() self.parameters = RequestParameters( @@ -38,50 +40,50 @@ def __init__(self, request, path_pattern=None): @property def host_url(self) -> str: - """Return request host url.""" + """Returns the request host URL.""" return f"{self._url.scheme}://{self._url.netloc}" @property def path(self) -> str: - """Return request path.""" + """Returns the request path.""" assert isinstance(self._url.path, str) return self._url.path -class OpenAPIResponse(): - """requests.Response wrapper for openapi_core.""" +class OpenAPIResponse: + """A requests.Response wrapper for openapi_core.""" def __init__(self, response): self.response = response msg = Message() - msg['content-type'] = response.headers.get('Content-Type', '') + msg["content-type"] = response.headers.get("Content-Type", "") self.content_type = msg.get_content_type() @property def data(self): - """Response body""" + """The response body.""" return self.response.text @property def status_code(self): - """Response status_code""" + """The response status code.""" return self.response.status_code @property def mimetype(self): - """Response Content-Type""" + """The response Content-Type.""" return self.content_type @property def headers(self): - """Response Headers""" + """The response headers.""" return self.response.headers def response_spec_json(filename): - """Initialization response_validator for openapi.json.""" + """Initializes a response_validator for openapi.json.""" with open(filename, "r", encoding="utf-8") as openapi: return Spec.from_dict(json.load(openapi)) -__all__ = ["response_spec_json", "OpenAPIRequest", "OpenAPIResponse"] +__all__ = ["OpenAPIRequest", "OpenAPIResponse", "response_spec_json"] diff --git a/tests_integrity/support.py b/tests_integrity/support.py index b63071a..8cd451d 100644 --- a/tests_integrity/support.py +++ b/tests_integrity/support.py @@ -16,11 +16,11 @@ class TestError(RuntimeError): - """Support exception.""" + """A support exception.""" def start_server(request, example, env=None, close=True): - """Start web server with example.""" + """Starts a web server with an example application.""" process = None print("Starting wsgi application...") @@ -59,7 +59,7 @@ def start_server(request, example, env=None, close=True): def check_url(url, method="GET", status_code=200, allow_redirects=True, **kwargs): - """Do HTTP request and check status_code.""" + """Performs an HTTP request and checks the status code.""" session = kwargs.pop("session", None) timeout = kwargs.pop("timeout", None) if not session: # nechceme vytvářet session nadarmo @@ -81,7 +81,7 @@ def check_url(url, method="GET", status_code=200, allow_redirects=True, def check_api(url, method="GET", status_code=200, response_spec=None, **kwargs): - """Do HTTP API request and check status_code.""" + """Performs an HTTP API request and checks the status code.""" assert response_spec, "response_validator must be set" session = kwargs.pop("session", None) if not session: diff --git a/tests_integrity/test_digest.py b/tests_integrity/test_digest.py index 58c42f6..a4feae7 100644 --- a/tests_integrity/test_digest.py +++ b/tests_integrity/test_digest.py @@ -15,7 +15,7 @@ @fixture(scope="module") def url(request): - """URL (server fixture in fact).""" + """The URL (server fixture).""" val = environ.get("TEST_DIGEST_URL", "").strip('/') if val: return val @@ -31,44 +31,51 @@ def url(request): @fixture def admin_auth(): + """Fixture for admin authentication.""" return HTTPDigestAuth('admin', 'admin') @fixture def user_auth(): + """Fixture for user authentication.""" return HTTPDigestAuth('user', 'looser') @fixture def utf8_auth(): + """Fixture for UTF-8 user authentication.""" return HTTPDigestAuth('Ondřej', 'heslíčko') class TestDigest: - """Test http_digest example.""" + """Tests the http_digest example.""" def test_unauthorized(self, url): + """Tests unauthorized access to various endpoints.""" check_url(url+'/admin_zone', status_code=401) check_url(url+'/user_zone', status_code=401) check_url(url+'/user', status_code=401) check_url(url+'/foo/passwd', method='POST', status_code=401, data={}) def test_admin(self, url, admin_auth): + """Tests access to the admin zone with admin credentials.""" check_url(url+'/admin_zone', auth=admin_auth) check_url(url+'/admin_zone', params={"arg": 42}, auth=admin_auth) def test_user(self, url, user_auth): + """Tests access to the user zone with user credentials.""" check_url(url+'/user_zone', auth=user_auth) check_url(url+'/user_zone', params={"param": 'text'}, auth=user_auth) check_url(url+'/user', auth=user_auth) @mark.skip("https://github.com/psf/requests/issues/6102") def test_utf8(self, url, utf8_auth): - """Check UTF-8 characters in username.""" + """Checks UTF-8 characters in the username.""" check_url(url+'/user/utf-8', auth=utf8_auth) check_url(url+'/user/utf-8', params={"param": 'text'}, auth=utf8_auth) def test_foo(self, url): + """Tests changing the password for the 'foo' user.""" auth = HTTPDigestAuth('foo', 'bar') check_url(url+'/foo', params={"x": 123}, auth=auth) check_url(url+'/foo', auth=auth) @@ -81,13 +88,17 @@ def test_foo(self, url): check_url(url+'/foo', auth=auth) def test_spaces(self, url, user_auth): + """Tests URLs with spaces in the path.""" check_url(url+'/spaces%20in%20url', auth=user_auth) def test_diacritics(self, url, user_auth): + """Tests URLs with diacritics in the path.""" check_url(url+'/%C4%8De%C5%A1tina%20v%20url', auth=user_auth) def test_unicode_smile(self, url, user_auth): + """Tests URLs with Unicode smileys in the path.""" check_url(url+'/crazy%20in%20url%20%F0%9F%A4%AA', auth=user_auth) def test_unknown(self, url, user_auth): + """Tests access to an unknown endpoint.""" check_url(url+'/unknown', auth=user_auth, status_code=401) diff --git a/tests_integrity/test_json.py b/tests_integrity/test_json.py index 059a625..043dd46 100644 --- a/tests_integrity/test_json.py +++ b/tests_integrity/test_json.py @@ -1,4 +1,4 @@ -"""Integrity test for JSON test/example application.""" +"""Integrity tests for the JSON test/example application.""" from os import environ from os.path import dirname, join, pardir from time import time @@ -16,6 +16,7 @@ @fixture(scope="module") def server(request): + """Fixture for starting the JSON example server.""" value = environ.get("TEST_SIMPLE_JSON_URL", "").strip('/') if value: return value @@ -30,9 +31,10 @@ def server(request): class TestHeaders: - """Test right headers in response.""" + """Tests correct headers in the response.""" def test_headers_empty(self, server): + """Tests response headers for an empty request.""" res = check_url(server+"/test/headers") assert "X-Powered-By" in res.headers assert res.headers["Content-Type"] == "application/json" @@ -43,6 +45,7 @@ def test_headers_empty(self, server): assert data["Accept-MimeType"]["json"] is False def test_headers_ajax(self, server): + """Tests response headers for an AJAX request.""" res = check_url( server+"/test/headers", headers={'X-Requested-With': 'XMLHttpRequest', @@ -57,10 +60,11 @@ def test_headers_ajax(self, server): class TestRequest: - """Request has some attributes.""" + """Tests various request attributes.""" # pylint: disable=too-few-public-methods def test_timestamp(self, server): + """Tests the timestamp attribute of the request.""" now = time() res = check_url(server+"/timestamp") timestamp = res.json()["timestamp"] @@ -69,6 +73,7 @@ def test_timestamp(self, server): assert abs(now - timestamp) < 0.1 def test_json_request(self, server): + """Tests handling of JSON requests.""" data = [{"x": 124.2, "y": 100.1}] res = check_url(server+"/test/json", status_code=418, method="POST", json=data, timeout=1) @@ -77,6 +82,7 @@ def test_json_request(self, server): "request": data} def test_stream_request(self, server): + """Tests handling of stream requests.""" def generator(): yield b'[{' for i in range(5): @@ -94,48 +100,56 @@ def generator(): class TestResponse: - """Test right responses.""" + """Tests correct responses.""" def test_json_response(self, server): + """Tests a basic JSONResponse.""" res = check_url(server+"/test/json", status_code=418, timeout=1) assert res.json() == {"message": "I'm teapot :-)", "numbers": [0, 1, 2, 3, 4], "request": {}} def test_json_generator_response(self, server): + """Tests a JSONGeneratorResponse.""" res = check_url(server+"/test/json-generator", status_code=418) assert res.json() == {"message": "I'm teapot :-)", "numbers": [0, 1, 2, 3, 4], "request": {}} def test_json_unicode(self, server): + """Tests JSON response with Unicode characters.""" data = "čeština" res = check_url(server+"/test/json", status_code=418, method="POST", json=data) assert res.json()["request"] == data def test_json_unicode_struct(self, server): + """Tests JSON response with a Unicode structure.""" data = {"lang": "čeština"} res = check_url(server+"/test/json", status_code=418, method="POST", json=data) assert res.json()["request"] == data def test_raw_unicode(self, server): + """Tests raw Unicode in the response.""" data = '{"name": "Ondřej Tůma"}' res = check_url(server+"/unicode") assert res.text == data assert int(res.headers['Content-Length']) == len(data.encode("utf-8")) def test_dict(self, server): + """Tests returning a dictionary as a JSON response.""" res = check_url(server+"/dict") assert res.json() == {"route": "/dict", "type": "dict"} def test_list(self, server): + """Tests returning a list as a JSON response.""" res = check_url(server+"/list") assert res.json() == [["key", "value"], ["route", "/list"], ["type", "list"]] def test_bad_json_response(self, server): + """Tests handling of a bad JSON response.""" check_url(server+"/test/json", status_code=400, method="POST", data=b"abraka crash", headers={'Content-Type': 'application/json'}) diff --git a/tests_integrity/test_metrics.py b/tests_integrity/test_metrics.py index bd51214..08445fb 100644 --- a/tests_integrity/test_metrics.py +++ b/tests_integrity/test_metrics.py @@ -1,4 +1,4 @@ -"""Integrity test for metrics example.""" +"""Integrity tests for the metrics example.""" # pylint: disable=redefined-outer-name # pylint: disable=missing-function-docstring # pylint: disable=no-self-use @@ -13,7 +13,8 @@ @fixture(scope="module") def url(request): - """Return server url or if exists or start metrics application.""" + """Returns the server URL or starts the metrics application if it doesn't + exist.""" retval = environ.get("TEST_METRICS_URL", "").strip('/') if retval: yield retval @@ -31,15 +32,18 @@ def url(request): class TestMetrics(): """Tests for example endpoints.""" def test_root(self, url): + """Tests the root endpoint.""" res = check_url(url+"/", headers={'Accept': 'text/plain'}) assert res.headers["Content-Type"] == "text/plain" def test_metrics(self, url): + """Tests the metrics endpoint.""" res = check_url(url+"/metrics") assert res.headers["Content-Type"].startswith("application/json") def test_invalid_request(self, url): + """Tests an invalid request to an endpoint.""" check_url(url+"/json", status_code=400, method="POST", data={'message': 'invalid'}, headers={'Content-Type': 'application/json'}) diff --git a/tests_integrity/test_openapi.py b/tests_integrity/test_openapi.py index 8b8daac..501a9e0 100644 --- a/tests_integrity/test_openapi.py +++ b/tests_integrity/test_openapi.py @@ -1,4 +1,4 @@ -"""Tests for opanapi implementation""" +"""Tests for OpenAPI implementation.""" from os import environ from os.path import dirname, join, pardir @@ -18,6 +18,7 @@ @fixture(scope="module") def url(request): + """Fixture for starting the OpenAPI example server.""" url = environ.get("TEST_OPENAPI_URL", "").strip('/') if url: yield url @@ -34,27 +35,31 @@ def url(request): @fixture def session(url): + """Fixture for creating a session and logging in.""" session = Session() check_url(url+"/login", session=session, status_code=204) return session class TestOpenAPI(): - """OpenAPI tests""" + """OpenAPI tests.""" def test_plain_text(self, url): + """Tests the /plain_text endpoint.""" res = check_api(url+"/plain_text", headers={'Accept': 'text/plain'}, response_spec=SPEC) assert res.headers["Content-Type"] == "text/plain" def test_content_header(self, url): + """Tests the /response endpoint for Content-Type header.""" res = check_api(url+"/response", headers={'Accept': 'application/json'}, response_spec=SPEC) assert res.headers["Content-Type"] == "application/json" def test_json_arg_integer(self, url): + """Tests the /json/{arg} endpoint with an integer argument.""" res = check_api(url+"/json/42", headers={'Accept': 'application/json'}, response_spec=SPEC) @@ -63,6 +68,7 @@ def test_json_arg_integer(self, url): assert data.get("arg") == '42' def test_json_arg_float(self, url): + """Tests the /json/{arg} endpoint with a float argument.""" res = check_api(url+"/json/3.14", headers={'Accept': 'application/json'}, response_spec=SPEC) @@ -71,6 +77,7 @@ def test_json_arg_float(self, url): assert data.get("arg") == '3.14' def test_json_arg_string(self, url): + """Tests the /json/{arg} endpoint with an invalid string argument.""" res = check_api(url+"/json/ok", status_code=400, headers={'Accept': 'application/json'}, @@ -80,6 +87,7 @@ def test_json_arg_string(self, url): assert data.get("error") is not None def test_json_post_unicode(self, url): + """Tests the /json POST endpoint with Unicode data.""" data = "Česká Lípa" res = check_api(url+"/json", status_code=418, method="POST", json=data, @@ -87,6 +95,7 @@ def test_json_post_unicode(self, url): assert res.json()["request"] == data def test_json_post_unicode_struct(self, url): + """Tests the /json PUT endpoint with a Unicode struct.""" data = {"city": "Česká Lípa"} res = check_api(url+"/json", status_code=418, method="PUT", json=data, @@ -94,12 +103,14 @@ def test_json_post_unicode_struct(self, url): assert res.json()["request"] == data def test_invalid_post_data(self, url): + """Tests the /json POST endpoint with invalid data.""" check_api(url+"/json", status_code=400, method="POST", data=b'\0', headers={"Content-Type": "application/json"}, response_spec=SPEC) def test_arg_integer(self, url): + """Tests the /arg/{arg} endpoint with an integer argument.""" res = check_api(url+"/arg/42", headers={'Accept': 'application/json'}, response_spec=SPEC) @@ -108,6 +119,7 @@ def test_arg_integer(self, url): assert data.get("arg") == 42 def test_arg_float(self, url): + """Tests the /arg/{arg} endpoint with a float argument.""" res = check_api(url+"/arg/3.14", headers={'Accept': 'application/json'}, response_spec=SPEC) @@ -116,6 +128,7 @@ def test_arg_float(self, url): assert data.get("arg") == 3.14 def test_arg_uuid(self, url): + """Tests the /arg/{arg} endpoint with a UUID argument.""" res = check_api(url+"/arg/3e4666bf-d5e5-4aa7-b8ce-cefe41c7568a", headers={'Accept': 'application/json'}, response_spec=SPEC) @@ -124,6 +137,7 @@ def test_arg_uuid(self, url): assert data.get("arg") == "3e4666bf-d5e5-4aa7-b8ce-cefe41c7568a" def test_arg_string(self, url): + """Tests the /arg/{arg} endpoint with an invalid string argument.""" res = check_api(url+"/arg/ok", status_code=400, headers={'Accept': 'application/json'}, @@ -133,25 +147,33 @@ def test_arg_string(self, url): assert data.get("error") is not None def test_native_not_found(self, url): + """Tests a native 404 Not Found response.""" check_url(url+"/notexists_url", status_code=404) def test_native_method_not_allowed(self, url): + """Tests a native 405 Method Not Allowed response.""" check_url(url+"/plain_text", method="DELETE", status_code=405) def test_secrets_cookie(self, url, session): + """Tests the /check/login endpoint with a valid session cookie.""" check_api(url+"/check/login", method="GET", session=session, response_spec=SPEC) def test_secrets_no_cookie(self, url): + """Tests the /check/login endpoint without a session cookie, expecting + 401.""" check_api(url+"/check/login", method="GET", status_code=401, response_spec=SPEC) def test_secrets_api_key(self, url): + """Tests the /check/api-key endpoint with a valid API key.""" check_api(url+"/check/api-key", method="GET", headers={"API-Key": "xxx"}, response_spec=SPEC) def test_secrets_no_api_key(self, url): + """Tests the /check/api-key endpoint without an API key, expecting + 401.""" check_api(url+"/check/api-key", method="GET", status_code=401, response_spec=SPEC) diff --git a/tests_integrity/test_profile.py b/tests_integrity/test_profile.py index 3d56a91..26a06dd 100644 --- a/tests_integrity/test_profile.py +++ b/tests_integrity/test_profile.py @@ -1,4 +1,4 @@ -"""Integrity test for profiling JSON test/example application.""" +"""Integrity tests for profiling the JSON test/example application.""" from os import environ from os.path import dirname, join, pardir @@ -16,6 +16,7 @@ @fixture(scope="module") def server(request): + """Fixture for starting the profiling JSON example server.""" value = environ.get("TEST_SIMPLE_JSON_URL", "").strip('/') if value: return value @@ -31,9 +32,10 @@ def server(request): class TestRequest: - """Request has some attributes.""" + """Tests various request attributes related to profiling.""" # pylint: disable=too-few-public-methods def test_profile(self, server): + """Tests the /profile endpoint.""" res = check_url(server+"/profile") assert res.json()["PROFILE"] == "1" diff --git a/tests_integrity/test_simple.py b/tests_integrity/test_simple.py index 9ac6e69..fb104de 100644 --- a/tests_integrity/test_simple.py +++ b/tests_integrity/test_simple.py @@ -1,4 +1,4 @@ -"""Base integrity test""" +"""Base integrity tests.""" from os import environ from os.path import dirname, join, pardir @@ -16,7 +16,7 @@ @fixture(scope="module") def url(request): - """URL (server fixture in fact).""" + """The URL (server fixture).""" url = environ.get("TEST_SIMPLE_URL", "").strip('/') if url: return url @@ -32,6 +32,7 @@ def url(request): @fixture def session(url): + """Fixture for creating a session and logging in.""" session = Session() res = check_url(url+"/login", status_code=302, session=session, allow_redirects=False) @@ -42,46 +43,58 @@ def session(url): class TestSimple(): - """Test for routes.""" + """Tests for routes.""" def test_root(self, url): + """Tests the root endpoint.""" check_url(url) def test_static(self, url): + """Tests the /test/static endpoint.""" check_url(url+"/test/static") def test_static_not_modified(self, url): + """Tests the /test/static endpoint with If-None-Match header for Not + Modified.""" res = check_url(url+"/test/static") check_url(url+"/test/static", status_code=304, headers={'ETag': res.headers.get('ETag')}) def test_exception_not_modified(self, url): + """Tests the /not-modified endpoint for Not Modified.""" check_url(url+"/not-modified", status_code=304) def test_variable_int(self, url): + """Tests a route with an integer variable.""" check_url(url+"/test/123") def test_variable_float(self, url): + """Tests a route with a float variable.""" check_url(url+"/test/123.679") def test_variable_user(self, url): + """Tests a route with a user (email) variable.""" check_url(url+"/test/teste@tester.net") def test_variable_uuid(self, url): + """Tests a route with a UUID variable.""" check_url(url+"/test/123e4567-e89b-12d3-a456-426655440000") def test_variable_uuid_upper(self, url): + """Tests a route with an uppercase UUID variable.""" check_url(url+"/test/123E4567-E89B-12D3-A456-426655440000") def test_debug_info(self, url): + """Tests the /debug-info endpoint.""" check_url(url+"/debug-info") class TestRequest: - """Test for requests.""" + """Tests for requests.""" # pylint: disable=too-few-public-methods def test_stream_request(self, url): + """Tests stream requests.""" def generator(): for i in range(5): yield b'%i' % i @@ -90,13 +103,14 @@ def generator(): class TestResponses(): - """Tests for Responses""" + """Tests for Responses.""" def test_yield(self, url): - """yield function is done by GeneratorResponse.""" + """Tests the yield function with GeneratorResponse.""" check_url(url+"/yield") def test_file_obj_response(self, url): + """Tests FileObjResponse.""" res = check_url(url+"/simple") assert 'Content-Length' in res.headers assert 'StorageFactory' in res.text @@ -104,6 +118,7 @@ def test_file_obj_response(self, url): assert '@app.before_response' in res.text def test_file_response(self, url): + """Tests FileResponse.""" res = check_url(url+"/simple.py") assert 'Content-Length' in res.headers assert 'StorageFactory' in res.text @@ -111,6 +126,8 @@ def test_file_response(self, url): assert '@app.before_response' in res.text def test_file_response_304_last_modified(self, url): + """Tests FileResponse with If-Modified-Since header for 304 Not + Modified.""" res = check_url(url+"/simple.py") last_modified = res.headers.get('Last-Modified') res = check_url(url+"/simple.py", @@ -118,6 +135,7 @@ def test_file_response_304_last_modified(self, url): status_code=304) def test_file_response_304_etag(self, url): + """Tests FileResponse with ETag header for 304 Not Modified.""" res = check_url(url+"/simple.py") etag = res.headers.get('ETag') res = check_url(url+"/simple.py", @@ -125,14 +143,15 @@ def test_file_response_304_etag(self, url): status_code=304) def test_none_no_content(self, url): - """Test debug output - which handler crash on none result.""" + """Tests None response resulting in 204 No Content.""" check_url(url+"/none", status_code=204) class TestPartialResponse(): - """Tests for Partial Responses""" + """Tests for Partial Responses.""" def test_file(self, url): + """Tests partial file response.""" res = check_url(url+"/simple.py", headers={'Range': 'bytes=-100'}, status_code=206) @@ -140,17 +159,22 @@ def test_file(self, url): assert res.text[-22:] == "httpd.serve_forever()\n" def test_empty_response(self, url): + """Tests the /test/empty endpoint for 204 No Content.""" check_url("{url}/test/empty".format(url=url), status_code=204) def test_empty(self, url): + """Tests the /test/partial/empty endpoint with no range.""" check_url("{url}/test/partial/empty".format(url=url), status_code=200) def test_empty_first_100(self, url): + """Tests the /test/partial/empty endpoint with a bytes range, expecting + 416.""" check_url("{url}/test/partial/empty".format(url=url), headers={'Range': 'bytes=0-99'}, status_code=416) def test_first_15(self, url): + """Tests a partial generator response for the first 15 bytes.""" res = check_url("{url}/test/partial/generator".format(url=url), headers={'Range': 'bytes=0-14'}, status_code=206) @@ -158,6 +182,7 @@ def test_first_15(self, url): assert res.text[-22:] == "line 0\nline 1\nl" def test_last_15(self, url): + """Tests a partial generator response for the last 15 bytes.""" res = check_url("{url}/test/partial/generator".format(url=url), headers={'Range': 'bytes=-15'}, status_code=206) @@ -165,6 +190,7 @@ def test_last_15(self, url): assert res.text[-22:] == "\nline 8\nline 9\n" def test_unicodes(self, url): + """Tests a partial response with Unicode range units.""" res = check_url("{url}/test/partial/unicodes".format(url=url), headers={'Range': 'unicodes=50-99'}, status_code=206) @@ -176,9 +202,11 @@ class TestSession(): """Session tests.""" def test_login(self, url): + """Tests the /login endpoint.""" check_url(url+"/login", status_code=302, allow_redirects=False) def test_logout(self, url, session): + """Tests the /logout endpoint.""" res = check_url(url+"/logout", session=session, allow_redirects=False, status_code=302) assert "SESSID" not in session.cookies # cookie is expired @@ -186,16 +214,20 @@ def test_logout(self, url, session): assert "; HttpOnly; " in cookie def test_form_get_not_logged(self, url): + """Tests GET form access when not logged in.""" check_url(url+"/test/form", status_code=302, allow_redirects=False) def test_form_get_logged(self, url, session): + """Tests GET form access when logged in.""" check_url(url+"/test/form", session=session, allow_redirects=False) def test_form_post(self, url, session): + """Tests POST form submission.""" check_url(url+"/test/form", method="POST", session=session, allow_redirects=False) def test_form_upload(self, url, session): + """Tests file upload via form.""" with open(__file__, 'rb') as _file: files = {'file_0': ('testfile.py', _file, 'text/x-python', {'Expires': '0'})} @@ -206,6 +238,7 @@ def test_form_upload(self, url, session): assert 'anything' in res.text def test_form_upload_small(self, url, session): + """Tests small file upload via form.""" manifest = join(dirname(__file__), pardir, 'MANIFEST.in') with open(manifest, 'rb') as _file: files = {'file_0': ('MANIFEST.in', _file, @@ -218,22 +251,29 @@ def test_form_upload_small(self, url, session): class TestErrors(): - """Integrity tests for native http state handlers.""" + """Integrity tests for native HTTP state handlers.""" def test_internal_server_error(self, url): + """Tests the /internal-server-error endpoint.""" check_url(url+"/internal-server-error", status_code=500) def test_bad_request(self, url): + """Tests the /bad-request endpoint.""" check_url(url+"/bad-request", status_code=400) def test_forbidden(self, url): + """Tests the /forbidden endpoint.""" check_url(url+"/forbidden", status_code=403) def test_not_found(self, url): + """Tests the /no-page endpoint for 404 Not Found.""" check_url(url+"/no-page", status_code=404) def test_method_not_allowed(self, url): + """Tests the /internal-server-error endpoint with an disallowed + method.""" check_url(url+"/internal-server-error", method="PUT", status_code=405) def test_not_implemented(self, url): + """Tests the /not-implemented endpoint.""" check_url(url+"/not-implemented", status_code=501) diff --git a/tests_integrity/test_websocket.py b/tests_integrity/test_websocket.py index 99bf8cb..5427099 100644 --- a/tests_integrity/test_websocket.py +++ b/tests_integrity/test_websocket.py @@ -1,4 +1,4 @@ -"""HTTP Digest example test.""" +"""WebSocket example tests.""" from os import environ from os.path import dirname, join, pardir from uuid import uuid1 @@ -18,6 +18,7 @@ @fixture(scope="module") def server(request): + """Fixture for starting the WebSocket example server.""" value = environ.get("TEST_WEBSOCKET_URL", "").strip('/') if value: return value @@ -34,18 +35,21 @@ def server(request): @fixture(scope="module") def http_url(server): + """Fixture for the HTTP URL of the server.""" return f"http://{server}" @fixture(scope="module") def ws_url(server): + """Fixture for the WebSocket URL of the server.""" return f"ws://{server}" class TestWebSocket: - """Test for WebSocket example.""" + """Tests for the WebSocket example.""" def test_upgrade(self, http_url): + """Tests the WebSocket upgrade handshake.""" uuid = uuid1().bytes check_url(http_url+"/ws", status_code=101, headers={"Connection": "Upgrade", @@ -55,6 +59,7 @@ def test_upgrade(self, http_url): encodebytes(uuid).decode().strip()}) def test_websocket(self, ws_url): + """Tests basic WebSocket communication.""" # python websocket library breaks usage websocket-client with pylint # pylint: disable=no-member wsck = WebSocket()