pytest_httpserver 1.0.10__tar.gz → 1.0.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/CHANGES.rst +17 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/PKG-INFO +1 -1
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/api.rst +23 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/conf.py +4 -2
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/howto.rst +146 -0
- pytest_httpserver-1.0.11/doc/patch.py +16 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pyproject.toml +25 -25
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/__init__.py +3 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/blocking_httpserver.py +2 -2
- pytest_httpserver-1.0.11/pytest_httpserver/hooks.py +103 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/httpserver.py +98 -9
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_custom_handler.py +2 -2
- pytest_httpserver-1.0.11/tests/examples/test_howto_custom_hooks.py +17 -0
- pytest_httpserver-1.0.11/tests/examples/test_howto_hooks.py +11 -0
- pytest_httpserver-1.0.11/tests/examples/test_howto_log_querying.py +56 -0
- pytest_httpserver-1.0.11/tests/test_hooks.py +102 -0
- pytest_httpserver-1.0.11/tests/test_log_querying.py +73 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_permanent.py +1 -1
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_release.py +16 -4
- pytest_httpserver-1.0.11/tests/test_threaded.py +60 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/CONTRIBUTION.md +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/LICENSE +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/README.md +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/Makefile +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/_static/.placeholder +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/background.rst +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/changes.rst +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/fixtures.rst +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/guide.rst +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/index.rst +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/tutorial.rst +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/upgrade.rst +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/example.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/example_pytest.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/py.typed +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/pytest_plugin.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/Makefile +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/README +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.cnf +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.crt +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.key +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.srl +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.cnf +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.crt +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.csr +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.key +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/v3.ext +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/conftest.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_example_blocking_httpserver.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_example_query_params1.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_example_query_params2.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_authorization_headers.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_case_insensitive_matcher.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_check.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_check_handler_errors.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_header_value_matcher.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_json_matcher.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_query_params_dict.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_query_params_never_do_this.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_query_params_proper_use.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_regexp.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_timeout_requests.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_url_matcher.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_wait_success.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_blocking_httpserver.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_handler_errors.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_headers.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_ip_protocols.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_json_matcher.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_mixed.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_oneshot.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_ordered.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_parse_qs.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_port_changing.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_querymatcher.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_querystring.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_ssl.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_urimatch.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_wait.py +0 -0
- {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_with_statement.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
Release Notes
|
|
3
3
|
=============
|
|
4
4
|
|
|
5
|
+
.. _Release Notes_1.0.11:
|
|
6
|
+
|
|
7
|
+
1.0.11
|
|
8
|
+
======
|
|
9
|
+
|
|
10
|
+
.. _Release Notes_1.0.11_New Features:
|
|
11
|
+
|
|
12
|
+
New Features
|
|
13
|
+
------------
|
|
14
|
+
|
|
15
|
+
- Hooks API
|
|
16
|
+
|
|
17
|
+
- New methods added to query for matching requests in the log.
|
|
18
|
+
|
|
19
|
+
- Threading support to serve requests in parallel
|
|
20
|
+
|
|
21
|
+
|
|
5
22
|
.. _Release Notes_1.0.10:
|
|
6
23
|
|
|
7
24
|
1.0.10
|
|
@@ -24,6 +24,13 @@ RequestHandler
|
|
|
24
24
|
:inherited-members:
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
RequestMatcher
|
|
28
|
+
~~~~~~~~~~~~~~
|
|
29
|
+
|
|
30
|
+
.. autoclass:: RequestMatcher
|
|
31
|
+
:members:
|
|
32
|
+
|
|
33
|
+
|
|
27
34
|
BlockingHTTPServer
|
|
28
35
|
~~~~~~~~~~~~~~~~~~
|
|
29
36
|
|
|
@@ -93,3 +100,19 @@ by the user.
|
|
|
93
100
|
|
|
94
101
|
.. autoclass:: pytest_httpserver.httpserver.RequestHandlerList
|
|
95
102
|
:members:
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
pytest_httpserver.hooks
|
|
106
|
+
-----------------------
|
|
107
|
+
|
|
108
|
+
.. automodule:: pytest_httpserver.hooks
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
.. autoclass:: pytest_httpserver.hooks.Chain
|
|
112
|
+
:members:
|
|
113
|
+
|
|
114
|
+
.. autoclass:: pytest_httpserver.hooks.Delay
|
|
115
|
+
:members:
|
|
116
|
+
|
|
117
|
+
.. autoclass:: pytest_httpserver.hooks.Garbage
|
|
118
|
+
:members:
|
|
@@ -24,6 +24,7 @@ from typing import Dict
|
|
|
24
24
|
|
|
25
25
|
sys.path.insert(0, os.path.abspath(".."))
|
|
26
26
|
|
|
27
|
+
import doc.patch
|
|
27
28
|
|
|
28
29
|
# -- General configuration ------------------------------------------------
|
|
29
30
|
|
|
@@ -37,11 +38,12 @@ sys.path.insert(0, os.path.abspath(".."))
|
|
|
37
38
|
extensions = [
|
|
38
39
|
"sphinx.ext.autodoc",
|
|
39
40
|
"sphinx.ext.intersphinx",
|
|
41
|
+
"sphinx.ext.autosectionlabel",
|
|
40
42
|
]
|
|
41
43
|
|
|
42
44
|
intersphinx_mapping = {
|
|
43
45
|
"python": ("https://docs.python.org/3", (None, "python-inv.txt")),
|
|
44
|
-
"werkzeug": ("https://werkzeug.palletsprojects.com/en/
|
|
46
|
+
"werkzeug": ("https://werkzeug.palletsprojects.com/en/3.0.x", None),
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
# Add any paths that contain templates here, relative to this directory.
|
|
@@ -66,7 +68,7 @@ author = "Zsolt Cserna"
|
|
|
66
68
|
# built documents.
|
|
67
69
|
#
|
|
68
70
|
# The short X.Y version.
|
|
69
|
-
version = "1.0.
|
|
71
|
+
version = "1.0.11"
|
|
70
72
|
# The full version, including alpha/beta/rc tags.
|
|
71
73
|
release = version
|
|
72
74
|
|
|
@@ -512,3 +512,149 @@ Example:
|
|
|
512
512
|
|
|
513
513
|
.. literalinclude :: ../tests/examples/test_example_blocking_httpserver.py
|
|
514
514
|
:language: python
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
Querying the log
|
|
518
|
+
----------------
|
|
519
|
+
|
|
520
|
+
*pytest-httpserver* keeps a log of request-response pairs in a python list. This
|
|
521
|
+
log can be accessed by the ``log`` attibute of the httpserver instance, but
|
|
522
|
+
there are methods made specifically to query the log.
|
|
523
|
+
|
|
524
|
+
Each of the log querying methods accepts a
|
|
525
|
+
:py:class:`pytest_httpserver.RequestMatcher` object which uses the same matching
|
|
526
|
+
logic which is used by the server itself. Its parameters are the same to the
|
|
527
|
+
parameters specified for the server's `except_request` (and the similar) methods.
|
|
528
|
+
|
|
529
|
+
The methods for querying:
|
|
530
|
+
|
|
531
|
+
* :py:meth:`pytest_httpserver.HTTPServer.get_matching_requests_count` returns
|
|
532
|
+
how many requests are matching in the log as an int
|
|
533
|
+
|
|
534
|
+
* :py:meth:`pytest_httpserver.HTTPServer.assert_request_made` asserts the given
|
|
535
|
+
amount of requests are matching in the log. By default it checks for one (1)
|
|
536
|
+
request but other value can be specified. For example, 0 can be specified to
|
|
537
|
+
check for requests not made.
|
|
538
|
+
|
|
539
|
+
* :py:meth:`pytest_httpserver.HTTPServer.iter_matching_requests` is a generator
|
|
540
|
+
yielding Request-Response tuples of the matching entries in the log. This
|
|
541
|
+
offers greater flexibility (compared to the other methods)
|
|
542
|
+
|
|
543
|
+
Example:
|
|
544
|
+
|
|
545
|
+
.. literalinclude :: ../tests/examples/test_howto_log_querying.py
|
|
546
|
+
:language: python
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
Serving requests in parallel
|
|
550
|
+
----------------------------
|
|
551
|
+
|
|
552
|
+
*pytest-httpserver* serves the request in a single-threaded, blocking way. That
|
|
553
|
+
means that if multiple requests are made to it, those will be served one by one.
|
|
554
|
+
|
|
555
|
+
There can be cases where parallel processing is required, for those cases
|
|
556
|
+
*pytest-httpserver* allows running a server which start one thread per request
|
|
557
|
+
handler, so the requests are served in parallel way (depending on Global
|
|
558
|
+
Interpreter Lock this is not truly parallel, but from the I/O point of view it
|
|
559
|
+
is).
|
|
560
|
+
|
|
561
|
+
To set this up, you have two possibilities.
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
Overriding httpserver fixture
|
|
565
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
566
|
+
|
|
567
|
+
One is to customize how the HTTPServer object is created. This is possible by
|
|
568
|
+
defining the following fixture:
|
|
569
|
+
|
|
570
|
+
.. code:: python
|
|
571
|
+
|
|
572
|
+
@pytest.fixture(scope="session")
|
|
573
|
+
def make_httpserver() -> Iterable[HTTPServer]:
|
|
574
|
+
server = HTTPServer(threaded=True) # set threaded=True to enable thread support
|
|
575
|
+
server.start()
|
|
576
|
+
yield server
|
|
577
|
+
server.clear()
|
|
578
|
+
if server.is_running():
|
|
579
|
+
server.stop()
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
This will override the ``httpserver`` fixture in your tests.
|
|
583
|
+
|
|
584
|
+
Creating a different httpserver fixture
|
|
585
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
586
|
+
|
|
587
|
+
This way, you can create a different httpserver fixture and you can use it
|
|
588
|
+
besides the main one.
|
|
589
|
+
|
|
590
|
+
.. code:: python
|
|
591
|
+
|
|
592
|
+
@pytest.fixture()
|
|
593
|
+
def threaded() -> Iterable[HTTPServer]:
|
|
594
|
+
server = HTTPServer(threaded=True)
|
|
595
|
+
server.start()
|
|
596
|
+
yield server
|
|
597
|
+
server.clear()
|
|
598
|
+
if server.is_running():
|
|
599
|
+
server.stop()
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def test_threaded(threaded: HTTPServer): ...
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
This will start and stop the server for each tests, which causes about 0.5
|
|
606
|
+
seconds waiting when the server is stopped. It won't override the ``httpserver``
|
|
607
|
+
fixture so you can keep the original single-threaded behavior.
|
|
608
|
+
|
|
609
|
+
.. warning::
|
|
610
|
+
Handler threads which are still running when the test is finished, will be
|
|
611
|
+
left behind and won't be join()ed between the tests. If you want to ensure
|
|
612
|
+
that all threads are properly cleaned up and you want to wait for them,
|
|
613
|
+
consider using the second option (:ref:`Creating a different httpserver fixture`)
|
|
614
|
+
described above.
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
Adding side effects
|
|
618
|
+
-------------------
|
|
619
|
+
|
|
620
|
+
Sometimes there's a need to add side effects to the handling of the requests.
|
|
621
|
+
Such side effect could be adding some amount of delay to the serving or adding
|
|
622
|
+
some garbage to response data.
|
|
623
|
+
|
|
624
|
+
While these can be achieved by using
|
|
625
|
+
:py:meth:`pytest_httpserver.RequestHandler.respond_with_handler` where you can
|
|
626
|
+
implement your own function to serve the request, *pytest-httpserver* provides a
|
|
627
|
+
hooks API where you can add side effects to request handlers such as
|
|
628
|
+
:py:meth:`pytest_httpserver.RequestHandler.respond_with_json` and others.
|
|
629
|
+
This allows to use the existing API of registering handlers.
|
|
630
|
+
|
|
631
|
+
Example:
|
|
632
|
+
|
|
633
|
+
.. literalinclude :: ../tests/examples/test_howto_hooks.py
|
|
634
|
+
:language: python
|
|
635
|
+
|
|
636
|
+
:py:mod:`pytest_httpserver.hooks` module provides some pre-defined hooks to
|
|
637
|
+
use.
|
|
638
|
+
|
|
639
|
+
You can implement your own hook as well. The requirement is to have a callable
|
|
640
|
+
object (a function) ``Callable[[Request, Response], Response]``. In details:
|
|
641
|
+
|
|
642
|
+
* Parameter :py:class:`werkzeug.Request` which represents the request
|
|
643
|
+
sent by the client.
|
|
644
|
+
|
|
645
|
+
* Parameter :py:class:`werkzeug.Response` which represents the response
|
|
646
|
+
made by the handler.
|
|
647
|
+
|
|
648
|
+
* Returns a :py:class:`werkzeug.Response` object which represents the
|
|
649
|
+
response will be returned to the client.
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
Example:
|
|
653
|
+
|
|
654
|
+
.. literalinclude :: ../tests/examples/test_howto_custom_hooks.py
|
|
655
|
+
:language: python
|
|
656
|
+
|
|
657
|
+
``with_post_hook`` can be called multiple times, in this case *pytest-httpserver*
|
|
658
|
+
will register the hooks, and hooks will be called sequentially, one by one. Each
|
|
659
|
+
hook will receive the response what the previous hook returned, and the last
|
|
660
|
+
hook called will return the final response which will be sent back to the client.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# this is required to make sphinx able to find references for classes put inside
|
|
2
|
+
# typing.TYPE_CHECKING block
|
|
3
|
+
|
|
4
|
+
from ssl import SSLContext
|
|
5
|
+
|
|
6
|
+
from werkzeug import Request
|
|
7
|
+
from werkzeug import Response
|
|
8
|
+
|
|
9
|
+
import pytest_httpserver.blocking_httpserver
|
|
10
|
+
import pytest_httpserver.httpserver
|
|
11
|
+
|
|
12
|
+
pytest_httpserver.httpserver.SSLContext = SSLContext
|
|
13
|
+
pytest_httpserver.blocking_httpserver.SSLContext = SSLContext
|
|
14
|
+
|
|
15
|
+
pytest_httpserver.blocking_httpserver.Request = Request
|
|
16
|
+
pytest_httpserver.blocking_httpserver.Response = Response
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pytest_httpserver"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.11"
|
|
4
4
|
description = "pytest-httpserver is a httpserver for pytest"
|
|
5
5
|
authors = ["Zsolt Cserna <cserna.zsolt@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -38,43 +38,43 @@ pytest_httpserver = "pytest_httpserver.pytest_plugin"
|
|
|
38
38
|
optional = true
|
|
39
39
|
|
|
40
40
|
[tool.poetry.group.develop.dependencies]
|
|
41
|
-
pre-commit = "
|
|
42
|
-
requests = "
|
|
43
|
-
Sphinx = "
|
|
44
|
-
sphinx-rtd-theme = "
|
|
45
|
-
reno = "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
pytest = "
|
|
49
|
-
pytest-cov = ">=3,<5"
|
|
41
|
+
pre-commit = ">=2.20,<4.0"
|
|
42
|
+
requests = "*"
|
|
43
|
+
Sphinx = ">=5.1.1,<8.0.0"
|
|
44
|
+
sphinx-rtd-theme = ">=1,<3"
|
|
45
|
+
reno = "*"
|
|
46
|
+
types-requests = "*"
|
|
47
|
+
pytest = ">=7.1.3,<9.0.0"
|
|
48
|
+
pytest-cov = ">=3,<6"
|
|
50
49
|
coverage = ">=6.4.4,<8.0.0"
|
|
51
|
-
types-toml = "
|
|
50
|
+
types-toml = "*"
|
|
52
51
|
toml = "^0.10.2"
|
|
53
|
-
black = "
|
|
54
|
-
ruff = "
|
|
52
|
+
black = "*"
|
|
53
|
+
ruff = "*"
|
|
54
|
+
mypy = "*"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
[tool.poetry.group.doc]
|
|
58
58
|
optional = true
|
|
59
59
|
|
|
60
60
|
[tool.poetry.group.doc.dependencies]
|
|
61
|
-
Sphinx = "
|
|
62
|
-
sphinx-rtd-theme = "
|
|
61
|
+
Sphinx = ">=5.1.1,<8.0.0"
|
|
62
|
+
sphinx-rtd-theme = ">=1,<3"
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
[tool.poetry.group.test]
|
|
66
66
|
optional = true
|
|
67
67
|
|
|
68
68
|
[tool.poetry.group.test.dependencies]
|
|
69
|
-
pytest = "
|
|
70
|
-
pytest-cov = "
|
|
71
|
-
coverage = "
|
|
72
|
-
requests = "
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
pytest = "*"
|
|
70
|
+
pytest-cov = "*"
|
|
71
|
+
coverage = "*"
|
|
72
|
+
requests = "*"
|
|
73
|
+
types-requests = "*"
|
|
74
|
+
pre-commit = "*"
|
|
75
|
+
types-toml = "*"
|
|
76
|
+
toml = "*"
|
|
77
|
+
mypy = "*"
|
|
78
78
|
|
|
79
79
|
[build-system]
|
|
80
80
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -87,7 +87,7 @@ markers = [
|
|
|
87
87
|
]
|
|
88
88
|
|
|
89
89
|
[tool.mypy]
|
|
90
|
-
files = ["pytest_httpserver", "scripts", "tests"
|
|
90
|
+
files = ["pytest_httpserver", "scripts", "tests"]
|
|
91
91
|
implicit_reexport = false
|
|
92
92
|
|
|
93
93
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
This is package provides the main API for the pytest_httpserver package.
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
__all__ = [
|
|
6
7
|
"HTTPServer",
|
|
7
8
|
"HTTPServerError",
|
|
@@ -10,6 +11,7 @@ __all__ = [
|
|
|
10
11
|
"WaitingSettings",
|
|
11
12
|
"HeaderValueMatcher",
|
|
12
13
|
"RequestHandler",
|
|
14
|
+
"RequestMatcher",
|
|
13
15
|
"URIPattern",
|
|
14
16
|
"URI_DEFAULT",
|
|
15
17
|
"METHOD_ALL",
|
|
@@ -27,5 +29,6 @@ from .httpserver import HTTPServer
|
|
|
27
29
|
from .httpserver import HTTPServerError
|
|
28
30
|
from .httpserver import NoHandlerError
|
|
29
31
|
from .httpserver import RequestHandler
|
|
32
|
+
from .httpserver import RequestMatcher
|
|
30
33
|
from .httpserver import URIPattern
|
|
31
34
|
from .httpserver import WaitingSettings
|
{pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/blocking_httpserver.py
RENAMED
|
@@ -18,8 +18,8 @@ from pytest_httpserver.httpserver import URIPattern
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from ssl import SSLContext
|
|
20
20
|
|
|
21
|
-
from werkzeug
|
|
22
|
-
from werkzeug
|
|
21
|
+
from werkzeug import Request
|
|
22
|
+
from werkzeug import Response
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class BlockingRequestHandler(RequestHandlerBase):
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hooks for pytest-httpserver
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from werkzeug import Request
|
|
10
|
+
from werkzeug import Response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Chain:
|
|
14
|
+
"""
|
|
15
|
+
Combine multiple hooks into one callable object
|
|
16
|
+
|
|
17
|
+
Hooks specified will be called one by one.
|
|
18
|
+
|
|
19
|
+
Each hook will receive the response object made by the previous hook,
|
|
20
|
+
similar to reduce.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, *args: Callable[[Request, Response], Response]):
|
|
24
|
+
"""
|
|
25
|
+
:param *args: callable objects specified in the same order they should
|
|
26
|
+
be called.
|
|
27
|
+
"""
|
|
28
|
+
self._hooks = args
|
|
29
|
+
|
|
30
|
+
def __call__(self, request: Request, response: Response) -> Response:
|
|
31
|
+
"""
|
|
32
|
+
Calls the callable object one by one. The second and further callable
|
|
33
|
+
objects receive the response returned by the previous one, while the
|
|
34
|
+
first one receives the original response object.
|
|
35
|
+
"""
|
|
36
|
+
for hook in self._hooks:
|
|
37
|
+
response = hook(request, response)
|
|
38
|
+
return response
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Delay:
|
|
42
|
+
"""
|
|
43
|
+
Delays returning the response
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, seconds: float):
|
|
47
|
+
"""
|
|
48
|
+
:param seconds: seconds to sleep before returning the response
|
|
49
|
+
"""
|
|
50
|
+
self._seconds = seconds
|
|
51
|
+
|
|
52
|
+
def _sleep(self):
|
|
53
|
+
"""
|
|
54
|
+
Sleeps for the seconds specified in the constructor
|
|
55
|
+
"""
|
|
56
|
+
time.sleep(self._seconds)
|
|
57
|
+
|
|
58
|
+
def __call__(self, _request: Request, response: Response) -> Response:
|
|
59
|
+
"""
|
|
60
|
+
Delays returning the response object for the time specified in the
|
|
61
|
+
constructor. Returns the original response unmodified.
|
|
62
|
+
"""
|
|
63
|
+
self._sleep()
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Garbage:
|
|
68
|
+
def __init__(self, prefix_size: int = 0, suffix_size: int = 0):
|
|
69
|
+
"""
|
|
70
|
+
Adds random bytes to the beginning or to the end of the response data.
|
|
71
|
+
|
|
72
|
+
:param prefix_size: amount of random bytes to be added to the beginning
|
|
73
|
+
of the response data
|
|
74
|
+
|
|
75
|
+
:param suffix_size: amount of random bytes to be added to the end
|
|
76
|
+
of the response data
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
assert prefix_size >= 0, "prefix_size should be positive integer"
|
|
80
|
+
assert suffix_size >= 0, "suffix_size should be positive integer"
|
|
81
|
+
self._prefix_size = prefix_size
|
|
82
|
+
self._suffix_size = suffix_size
|
|
83
|
+
|
|
84
|
+
def _get_garbage_bytes(self, size: int) -> bytes:
|
|
85
|
+
"""
|
|
86
|
+
Returns the specified amount of random bytes.
|
|
87
|
+
|
|
88
|
+
:param size: amount of bytes to return
|
|
89
|
+
"""
|
|
90
|
+
return os.urandom(size)
|
|
91
|
+
|
|
92
|
+
def __call__(self, _request: Request, response: Response) -> Response:
|
|
93
|
+
"""
|
|
94
|
+
Adds random bytes to the beginning or to the end of the response data.
|
|
95
|
+
|
|
96
|
+
New random bytes will be generated for every call.
|
|
97
|
+
|
|
98
|
+
Returns the modified response object.
|
|
99
|
+
"""
|
|
100
|
+
prefix = self._get_garbage_bytes(self._prefix_size)
|
|
101
|
+
suffix = self._get_garbage_bytes(self._suffix_size)
|
|
102
|
+
response.set_data(prefix + response.get_data() + suffix)
|
|
103
|
+
return response
|
|
@@ -26,11 +26,11 @@ from typing import Tuple
|
|
|
26
26
|
from typing import Union
|
|
27
27
|
|
|
28
28
|
import werkzeug.http
|
|
29
|
+
from werkzeug import Request
|
|
30
|
+
from werkzeug import Response
|
|
29
31
|
from werkzeug.datastructures import Authorization
|
|
30
32
|
from werkzeug.datastructures import MultiDict
|
|
31
33
|
from werkzeug.serving import make_server
|
|
32
|
-
from werkzeug.wrappers import Request
|
|
33
|
-
from werkzeug.wrappers import Response
|
|
34
34
|
|
|
35
35
|
if TYPE_CHECKING:
|
|
36
36
|
from ssl import SSLContext
|
|
@@ -495,7 +495,7 @@ class RequestHandlerBase(abc.ABC):
|
|
|
495
495
|
"""
|
|
496
496
|
Prepares a response with raw data.
|
|
497
497
|
|
|
498
|
-
For detailed description please see the :py:class:`werkzeug.
|
|
498
|
+
For detailed description please see the :py:class:`werkzeug.Response` object as the
|
|
499
499
|
parameters are analogue.
|
|
500
500
|
|
|
501
501
|
:param response_data: a string or bytes object representing the body of the response
|
|
@@ -529,6 +529,11 @@ class RequestHandler(RequestHandlerBase):
|
|
|
529
529
|
def __init__(self, matcher: RequestMatcher):
|
|
530
530
|
self.matcher = matcher
|
|
531
531
|
self.request_handler: Callable[[Request], Response] | None = None
|
|
532
|
+
self._hooks: list[Callable[[Request, Response], Response]] = []
|
|
533
|
+
|
|
534
|
+
def with_post_hook(self, hook: Callable[[Request, Response], Response]):
|
|
535
|
+
self._hooks.append(hook)
|
|
536
|
+
return self
|
|
532
537
|
|
|
533
538
|
def respond(self, request: Request) -> Response:
|
|
534
539
|
"""
|
|
@@ -546,7 +551,11 @@ class RequestHandler(RequestHandlerBase):
|
|
|
546
551
|
"Matching request handler found but no response defined: {} {}".format(request.method, request.path)
|
|
547
552
|
)
|
|
548
553
|
else:
|
|
549
|
-
|
|
554
|
+
response = self.request_handler(request)
|
|
555
|
+
|
|
556
|
+
for hook in self._hooks:
|
|
557
|
+
response = hook(request, response)
|
|
558
|
+
return response
|
|
550
559
|
|
|
551
560
|
def respond_with_handler(self, func: Callable[[Request], Response]):
|
|
552
561
|
"""
|
|
@@ -598,11 +607,12 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
|
|
|
598
607
|
:param host: the host or IP where the server will listen
|
|
599
608
|
:param port: the TCP port where the server will listen
|
|
600
609
|
:param ssl_context: the ssl context object to use for https connections
|
|
610
|
+
:param threaded: whether to handle concurrent requests in separate threads
|
|
601
611
|
|
|
602
612
|
.. py:attribute:: log
|
|
603
613
|
|
|
604
614
|
Attribute containing the list of two-element tuples. Each tuple contains
|
|
605
|
-
:py:class:`werkzeug.
|
|
615
|
+
:py:class:`werkzeug.Request` and :py:class:`werkzeug.Response` object which represents the
|
|
606
616
|
incoming request and the outgoing response which happened during the lifetime
|
|
607
617
|
of the server.
|
|
608
618
|
|
|
@@ -619,6 +629,8 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
|
|
|
619
629
|
host: str,
|
|
620
630
|
port: int,
|
|
621
631
|
ssl_context: SSLContext | None = None,
|
|
632
|
+
*,
|
|
633
|
+
threaded: bool = False,
|
|
622
634
|
):
|
|
623
635
|
"""
|
|
624
636
|
Initializes the instance.
|
|
@@ -632,6 +644,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
|
|
|
632
644
|
self.handler_errors: list[Exception] = []
|
|
633
645
|
self.log: list[tuple[Request, Response]] = []
|
|
634
646
|
self.ssl_context = ssl_context
|
|
647
|
+
self.threaded = threaded
|
|
635
648
|
self.no_handler_status_code = 500
|
|
636
649
|
|
|
637
650
|
def __repr__(self):
|
|
@@ -730,11 +743,11 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
|
|
|
730
743
|
This method returns immediately (e.g. does not block), and it's the caller's
|
|
731
744
|
responsibility to stop the server (by calling :py:meth:`stop`) when it is no longer needed).
|
|
732
745
|
|
|
733
|
-
If the
|
|
746
|
+
If the server is not stopped by the caller and execution reaches the end, the
|
|
734
747
|
program needs to be terminated by Ctrl+C or by signal as it will not terminate until
|
|
735
748
|
the thread is stopped.
|
|
736
749
|
|
|
737
|
-
If the
|
|
750
|
+
If the server is already running :py:class:`HTTPServerError` will be raised. If you are
|
|
738
751
|
unsure, call :py:meth:`is_running` first.
|
|
739
752
|
|
|
740
753
|
There's a context interface of this class which stops the server when the context block ends.
|
|
@@ -742,7 +755,9 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
|
|
|
742
755
|
if self.is_running():
|
|
743
756
|
raise HTTPServerError("Server is already running")
|
|
744
757
|
|
|
745
|
-
self.server = make_server(
|
|
758
|
+
self.server = make_server(
|
|
759
|
+
self.host, self.port, self.application, ssl_context=self.ssl_context, threaded=self.threaded
|
|
760
|
+
)
|
|
746
761
|
self.port = self.server.port # Update port (needed if `port` was set to 0)
|
|
747
762
|
self.server_thread = threading.Thread(target=self.thread_target)
|
|
748
763
|
self.server_thread.start()
|
|
@@ -900,6 +915,8 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
|
|
|
900
915
|
:param default_waiting_settings: the waiting settings object to use as default settings for :py:meth:`wait` context
|
|
901
916
|
manager
|
|
902
917
|
|
|
918
|
+
:param threaded: whether to handle concurrent requests in separate threads
|
|
919
|
+
|
|
903
920
|
.. py:attribute:: no_handler_status_code
|
|
904
921
|
|
|
905
922
|
Attribute containing the http status code (int) which will be the response
|
|
@@ -916,11 +933,13 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
|
|
|
916
933
|
port=DEFAULT_LISTEN_PORT,
|
|
917
934
|
ssl_context: SSLContext | None = None,
|
|
918
935
|
default_waiting_settings: WaitingSettings | None = None,
|
|
936
|
+
*,
|
|
937
|
+
threaded: bool = False,
|
|
919
938
|
):
|
|
920
939
|
"""
|
|
921
940
|
Initializes the instance.
|
|
922
941
|
"""
|
|
923
|
-
super().__init__(host, port, ssl_context)
|
|
942
|
+
super().__init__(host, port, ssl_context, threaded=threaded)
|
|
924
943
|
|
|
925
944
|
self.ordered_handlers: list[RequestHandler] = []
|
|
926
945
|
self.oneshot_handlers = RequestHandlerList()
|
|
@@ -1333,3 +1352,73 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
|
|
|
1333
1352
|
)
|
|
1334
1353
|
if self._waiting_settings.raise_assertions and not waiting.result:
|
|
1335
1354
|
self.check_assertions()
|
|
1355
|
+
|
|
1356
|
+
def iter_matching_requests(self, matcher: RequestMatcher) -> Iterable[tuple[Request, Response]]:
|
|
1357
|
+
"""
|
|
1358
|
+
Queries log for matching requests.
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
:param matcher: the matcher object to match requests
|
|
1362
|
+
:return: an iterator with request-response pair from the log
|
|
1363
|
+
"""
|
|
1364
|
+
|
|
1365
|
+
for request, response in self.log:
|
|
1366
|
+
if matcher.match(request):
|
|
1367
|
+
yield (request, response)
|
|
1368
|
+
|
|
1369
|
+
def get_matching_requests_count(self, matcher: RequestMatcher) -> int:
|
|
1370
|
+
"""
|
|
1371
|
+
Queries the log for matching requests, returning the number of log
|
|
1372
|
+
entries matching for the specified matcher.
|
|
1373
|
+
|
|
1374
|
+
:param matcher: the matcher object to match requests
|
|
1375
|
+
:return: the number of log entries matching
|
|
1376
|
+
"""
|
|
1377
|
+
return len(list(self.iter_matching_requests(matcher)))
|
|
1378
|
+
|
|
1379
|
+
def assert_request_made(self, matcher: RequestMatcher, *, count: int = 1):
|
|
1380
|
+
"""
|
|
1381
|
+
Check the amount of log entries matching for the matcher specified. By
|
|
1382
|
+
default it verifies that exactly one request matching for the matcher
|
|
1383
|
+
specified. The expected count can be customized with the count kwarg
|
|
1384
|
+
(including zero, which asserts that no requests made for the given
|
|
1385
|
+
matcher).
|
|
1386
|
+
|
|
1387
|
+
:param matcher: the matcher object to match requests
|
|
1388
|
+
:param count: the expected number of matches in the log
|
|
1389
|
+
:return: ``None`` if the assert succeeded, raises
|
|
1390
|
+
:py:class:`AssertionError` if not.
|
|
1391
|
+
"""
|
|
1392
|
+
|
|
1393
|
+
matching_count = self.get_matching_requests_count(matcher)
|
|
1394
|
+
if matching_count != count:
|
|
1395
|
+
similar_requests: list[Request] = []
|
|
1396
|
+
for request, _ in self.log:
|
|
1397
|
+
if request.path == matcher.uri:
|
|
1398
|
+
similar_requests.append(request)
|
|
1399
|
+
|
|
1400
|
+
assert_msg_lines = [
|
|
1401
|
+
f"Matching request found {matching_count} times but expected {count} times.",
|
|
1402
|
+
f"Expected request: {matcher}",
|
|
1403
|
+
]
|
|
1404
|
+
|
|
1405
|
+
if similar_requests:
|
|
1406
|
+
assert_msg_lines.append(f"Found {len(similar_requests)} similar request(s):")
|
|
1407
|
+
for request in similar_requests:
|
|
1408
|
+
assert_msg_lines.extend(
|
|
1409
|
+
(
|
|
1410
|
+
"--- Similar Request Start",
|
|
1411
|
+
f"Path: {request.path}",
|
|
1412
|
+
f"Method: {request.method}",
|
|
1413
|
+
f"Body: {request.get_data()!r}",
|
|
1414
|
+
f"Headers: {request.headers}",
|
|
1415
|
+
f"Query String: {request.query_string.decode('utf-8')!r}",
|
|
1416
|
+
"--- Similar Request End",
|
|
1417
|
+
)
|
|
1418
|
+
)
|
|
1419
|
+
else:
|
|
1420
|
+
assert_msg_lines.append("No similar requests found.")
|
|
1421
|
+
|
|
1422
|
+
assert_msg = "\n".join(assert_msg_lines) + "\n"
|
|
1423
|
+
|
|
1424
|
+
assert matching_count == count, assert_msg
|