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.
Files changed (80) hide show
  1. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/CHANGES.rst +17 -0
  2. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/PKG-INFO +1 -1
  3. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/api.rst +23 -0
  4. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/conf.py +4 -2
  5. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/howto.rst +146 -0
  6. pytest_httpserver-1.0.11/doc/patch.py +16 -0
  7. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pyproject.toml +25 -25
  8. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/__init__.py +3 -0
  9. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/blocking_httpserver.py +2 -2
  10. pytest_httpserver-1.0.11/pytest_httpserver/hooks.py +103 -0
  11. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/httpserver.py +98 -9
  12. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_custom_handler.py +2 -2
  13. pytest_httpserver-1.0.11/tests/examples/test_howto_custom_hooks.py +17 -0
  14. pytest_httpserver-1.0.11/tests/examples/test_howto_hooks.py +11 -0
  15. pytest_httpserver-1.0.11/tests/examples/test_howto_log_querying.py +56 -0
  16. pytest_httpserver-1.0.11/tests/test_hooks.py +102 -0
  17. pytest_httpserver-1.0.11/tests/test_log_querying.py +73 -0
  18. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_permanent.py +1 -1
  19. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_release.py +16 -4
  20. pytest_httpserver-1.0.11/tests/test_threaded.py +60 -0
  21. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/CONTRIBUTION.md +0 -0
  22. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/LICENSE +0 -0
  23. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/README.md +0 -0
  24. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/Makefile +0 -0
  25. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/_static/.placeholder +0 -0
  26. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/background.rst +0 -0
  27. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/changes.rst +0 -0
  28. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/fixtures.rst +0 -0
  29. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/guide.rst +0 -0
  30. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/index.rst +0 -0
  31. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/tutorial.rst +0 -0
  32. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/doc/upgrade.rst +0 -0
  33. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/example.py +0 -0
  34. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/example_pytest.py +0 -0
  35. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/py.typed +0 -0
  36. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/pytest_httpserver/pytest_plugin.py +0 -0
  37. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/Makefile +0 -0
  38. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/README +0 -0
  39. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.cnf +0 -0
  40. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.crt +0 -0
  41. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.key +0 -0
  42. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/rootCA.srl +0 -0
  43. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.cnf +0 -0
  44. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.crt +0 -0
  45. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.csr +0 -0
  46. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/server.key +0 -0
  47. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/assets/v3.ext +0 -0
  48. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/conftest.py +0 -0
  49. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_example_blocking_httpserver.py +0 -0
  50. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_example_query_params1.py +0 -0
  51. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_example_query_params2.py +0 -0
  52. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_authorization_headers.py +0 -0
  53. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_case_insensitive_matcher.py +0 -0
  54. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_check.py +0 -0
  55. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_check_handler_errors.py +0 -0
  56. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_header_value_matcher.py +0 -0
  57. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_json_matcher.py +0 -0
  58. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_query_params_dict.py +0 -0
  59. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_query_params_never_do_this.py +0 -0
  60. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_query_params_proper_use.py +0 -0
  61. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_regexp.py +0 -0
  62. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_timeout_requests.py +0 -0
  63. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_url_matcher.py +0 -0
  64. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/examples/test_howto_wait_success.py +0 -0
  65. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_blocking_httpserver.py +0 -0
  66. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_handler_errors.py +0 -0
  67. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_headers.py +0 -0
  68. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_ip_protocols.py +0 -0
  69. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_json_matcher.py +0 -0
  70. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_mixed.py +0 -0
  71. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_oneshot.py +0 -0
  72. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_ordered.py +0 -0
  73. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_parse_qs.py +0 -0
  74. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_port_changing.py +0 -0
  75. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_querymatcher.py +0 -0
  76. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_querystring.py +0 -0
  77. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_ssl.py +0 -0
  78. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_urimatch.py +0 -0
  79. {pytest_httpserver-1.0.10 → pytest_httpserver-1.0.11}/tests/test_wait.py +0 -0
  80. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytest_httpserver
3
- Version: 1.0.10
3
+ Version: 1.0.11
4
4
  Summary: pytest-httpserver is a httpserver for pytest
5
5
  Home-page: https://github.com/csernazs/pytest-httpserver
6
6
  License: MIT
@@ -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/2.1.x", None),
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.10"
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.10"
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 = "^2.20.0"
42
- requests = "^2.28.1"
43
- Sphinx = "^5.1.1"
44
- sphinx-rtd-theme = "^1.0.0"
45
- reno = "^3.5.0"
46
- mypy = "^0.971"
47
- types-requests = "^2.28.9"
48
- pytest = "^7.1.3"
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 = "^0.10.8"
50
+ types-toml = "*"
52
51
  toml = "^0.10.2"
53
- black = "^23.1.0"
54
- ruff = "^0.2.1"
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 = "^5.1.1"
62
- sphinx-rtd-theme = "^1.0.0"
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 = "^7.1.3"
70
- pytest-cov = ">=3,<5"
71
- coverage = ">=6.4.4,<8.0.0"
72
- requests = "^2.28.1"
73
- mypy = "^0.971"
74
- types-requests = "^2.28.9"
75
- pre-commit = "^2.20.0"
76
- types-toml = "^0.10.8"
77
- toml = "^0.10.2"
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", "doc"]
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
@@ -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.wrappers import Request
22
- from werkzeug.wrappers import Response
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.wrappers.Response` object as the
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
- return self.request_handler(request)
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.wrappers.Request` and :py:class:`werkzeug.wrappers.Response` object which represents the
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 sever is not stopped by the caller and execution reaches the end, 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 sever is already running :py:class:`HTTPServerError` will be raised. If you are
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(self.host, self.port, self.application, ssl_context=self.ssl_context)
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