pytest_httpserver 1.1.0__tar.gz → 1.1.2__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 (83) hide show
  1. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/CHANGES.rst +28 -0
  2. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/PKG-INFO +4 -4
  3. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/README.md +1 -1
  4. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/conf.py +1 -1
  5. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/howto.rst +11 -0
  6. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/pyproject.toml +7 -9
  7. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/pytest_httpserver/blocking_httpserver.py +2 -2
  8. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/pytest_httpserver/httpserver.py +73 -40
  9. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/pytest_httpserver/pytest_plugin.py +3 -3
  10. pytest_httpserver-1.1.2/tests/examples/test_howto_custom_request_matcher.py +37 -0
  11. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_blocking_httpserver.py +2 -2
  12. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_log_leak.py +1 -1
  13. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_log_querying.py +1 -1
  14. pytest_httpserver-1.1.2/tests/test_matcher.py +11 -0
  15. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_mixed.py +1 -1
  16. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_port_changing.py +1 -1
  17. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_release.py +15 -5
  18. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_ssl.py +11 -4
  19. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_threaded.py +2 -10
  20. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/CONTRIBUTION.md +0 -0
  21. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/LICENSE +0 -0
  22. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/Makefile +0 -0
  23. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/_static/.placeholder +0 -0
  24. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/api.rst +0 -0
  25. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/background.rst +0 -0
  26. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/changes.rst +0 -0
  27. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/fixtures.rst +0 -0
  28. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/guide.rst +0 -0
  29. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/index.rst +0 -0
  30. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/patch.py +0 -0
  31. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/tutorial.rst +0 -0
  32. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/doc/upgrade.rst +0 -0
  33. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/example.py +0 -0
  34. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/example_pytest.py +0 -0
  35. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/pytest_httpserver/__init__.py +7 -7
  36. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/pytest_httpserver/hooks.py +0 -0
  37. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/pytest_httpserver/py.typed +0 -0
  38. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/Makefile +0 -0
  39. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/README +0 -0
  40. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/rootCA.cnf +0 -0
  41. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/rootCA.crt +0 -0
  42. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/rootCA.key +0 -0
  43. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/rootCA.srl +0 -0
  44. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/server.cnf +0 -0
  45. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/server.crt +0 -0
  46. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/server.csr +0 -0
  47. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/server.key +0 -0
  48. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/assets/v3.ext +0 -0
  49. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/conftest.py +0 -0
  50. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_example_blocking_httpserver.py +0 -0
  51. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_example_query_params1.py +0 -0
  52. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_example_query_params2.py +0 -0
  53. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_authorization_headers.py +0 -0
  54. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_case_insensitive_matcher.py +0 -0
  55. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_check.py +0 -0
  56. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_check_handler_errors.py +0 -0
  57. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_custom_handler.py +0 -0
  58. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_custom_hooks.py +0 -0
  59. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_header_value_matcher.py +0 -0
  60. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_hooks.py +0 -0
  61. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_json_matcher.py +0 -0
  62. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_log_querying.py +0 -0
  63. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_query_params_dict.py +0 -0
  64. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_query_params_never_do_this.py +0 -0
  65. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_query_params_proper_use.py +0 -0
  66. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_regexp.py +0 -0
  67. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_timeout_requests.py +0 -0
  68. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_url_matcher.py +0 -0
  69. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/examples/test_howto_wait_success.py +0 -0
  70. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_handler_errors.py +0 -0
  71. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_headers.py +0 -0
  72. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_hooks.py +0 -0
  73. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_ip_protocols.py +0 -0
  74. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_json_matcher.py +0 -0
  75. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_oneshot.py +0 -0
  76. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_ordered.py +0 -0
  77. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_parse_qs.py +0 -0
  78. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_permanent.py +0 -0
  79. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_querymatcher.py +0 -0
  80. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_querystring.py +0 -0
  81. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_urimatch.py +0 -0
  82. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_wait.py +0 -0
  83. {pytest_httpserver-1.1.0 → pytest_httpserver-1.1.2}/tests/test_with_statement.py +0 -0
@@ -2,6 +2,34 @@
2
2
  Release Notes
3
3
  =============
4
4
 
5
+ .. _Release Notes_1.1.2:
6
+
7
+ 1.1.2
8
+ =====
9
+
10
+ .. _Release Notes_1.1.2_Deprecation Notes:
11
+
12
+ Deprecation Notes
13
+ -----------------
14
+
15
+ - Python versions earlier than 3.9 have been deprecated in order to make the
16
+ code more type safe. Python 3.8 has reached EOL on 2024-10-07.
17
+
18
+
19
+ .. _Release Notes_1.1.1:
20
+
21
+ 1.1.1
22
+ =====
23
+
24
+ .. _Release Notes_1.1.1_New Features:
25
+
26
+ New Features
27
+ ------------
28
+
29
+ - Add a new ``expect`` method to the ``HTTPServer`` object which allows
30
+ developers to provide their own request matcher object.
31
+
32
+
5
33
  .. _Release Notes_1.1.0:
6
34
 
7
35
  1.1.0
@@ -1,23 +1,23 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytest_httpserver
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: pytest-httpserver is a httpserver for pytest
5
5
  Home-page: https://github.com/csernazs/pytest-httpserver
6
6
  License: MIT
7
7
  Author: Zsolt Cserna
8
8
  Author-email: cserna.zsolt@gmail.com
9
- Requires-Python: >=3.8
9
+ Requires-Python: >=3.9
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Framework :: Pytest
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.8
17
16
  Classifier: Programming Language :: Python :: 3.9
18
17
  Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Requires-Dist: Werkzeug (>=2.0.0)
23
23
  Project-URL: Bug Tracker, https://github.com/csernazs/pytest-httpserver/issues
@@ -25,7 +25,7 @@ Project-URL: Documentation, https://pytest-httpserver.readthedocs.io/en/latest/
25
25
  Project-URL: Repository, https://github.com/csernazs/pytest-httpserver
26
26
  Description-Content-Type: text/markdown
27
27
 
28
- [![Build Status](https://github.com/csernazs/pytest-httpserver/workflows/build/badge.svg?branch=master)](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster)
28
+ [![Build Status](https://github.com/csernazs/pytest-httpserver/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster)
29
29
  [![Documentation Status](https://readthedocs.org/projects/pytest-httpserver/badge/?version=latest)](https://pytest-httpserver.readthedocs.io/en/latest/?badge=latest)
30
30
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
31
31
  [![codecov](https://codecov.io/gh/csernazs/pytest-httpserver/branch/master/graph/badge.svg?token=MX2JXbHqRH)](https://codecov.io/gh/csernazs/pytest-httpserver)
@@ -1,4 +1,4 @@
1
- [![Build Status](https://github.com/csernazs/pytest-httpserver/workflows/build/badge.svg?branch=master)](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster)
1
+ [![Build Status](https://github.com/csernazs/pytest-httpserver/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster)
2
2
  [![Documentation Status](https://readthedocs.org/projects/pytest-httpserver/badge/?version=latest)](https://pytest-httpserver.readthedocs.io/en/latest/?badge=latest)
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
4
  [![codecov](https://codecov.io/gh/csernazs/pytest-httpserver/branch/master/graph/badge.svg?token=MX2JXbHqRH)](https://codecov.io/gh/csernazs/pytest-httpserver)
@@ -68,7 +68,7 @@ author = "Zsolt Cserna"
68
68
  # built documents.
69
69
  #
70
70
  # The short X.Y version.
71
- version = "1.1.0"
71
+ version = "1.1.2"
72
72
  # The full version, including alpha/beta/rc tags.
73
73
  release = version
74
74
 
@@ -238,6 +238,17 @@ these.
238
238
  the server.
239
239
 
240
240
 
241
+ Using custom request matcher
242
+ ----------------------------
243
+ In the case when you want to extend or modify the request matcher in
244
+ *pytest-httpserrver*, then you can use your own request matcher.
245
+
246
+ Example:
247
+
248
+ .. literalinclude :: ../tests/examples/test_howto_custom_request_matcher.py
249
+ :language: python
250
+
251
+
241
252
  Customizing host and port
242
253
  -------------------------
243
254
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pytest_httpserver"
3
- version = "1.1.0"
3
+ version = "1.1.2"
4
4
  description = "pytest-httpserver is a httpserver for pytest"
5
5
  authors = ["Zsolt Cserna <cserna.zsolt@gmail.com>"]
6
6
  license = "MIT"
@@ -24,7 +24,7 @@ include = [
24
24
  ]
25
25
 
26
26
  [tool.poetry.dependencies]
27
- python = ">=3.8"
27
+ python = ">=3.9"
28
28
  Werkzeug = ">= 2.0.0"
29
29
 
30
30
 
@@ -41,14 +41,13 @@ optional = true
41
41
  pre-commit = ">=2.20,<4.0"
42
42
  requests = "*"
43
43
  Sphinx = ">=5.1.1,<8.0.0"
44
- sphinx-rtd-theme = ">=1,<3"
44
+ sphinx-rtd-theme = ">=1,<4"
45
45
  reno = "*"
46
46
  types-requests = "*"
47
47
  pytest = ">=7.1.3,<9.0.0"
48
48
  pytest-cov = ">=3,<6"
49
49
  coverage = ">=6.4.4,<8.0.0"
50
- types-toml = "*"
51
- toml = "^0.10.2"
50
+ tomli = { version = "*", markers = "python_version < '3.11'"}
52
51
  black = "*"
53
52
  ruff = "*"
54
53
  mypy = "*"
@@ -59,7 +58,7 @@ optional = true
59
58
 
60
59
  [tool.poetry.group.doc.dependencies]
61
60
  Sphinx = ">=5.1.1,<8.0.0"
62
- sphinx-rtd-theme = ">=1,<3"
61
+ sphinx-rtd-theme = ">=1,<4"
63
62
 
64
63
 
65
64
  [tool.poetry.group.test]
@@ -72,8 +71,7 @@ coverage = "*"
72
71
  requests = "*"
73
72
  types-requests = "*"
74
73
  pre-commit = "*"
75
- types-toml = "*"
76
- toml = "*"
74
+ tomli = { version = "*", markers = "python_version < '3.11'"}
77
75
  mypy = "*"
78
76
 
79
77
  [build-system]
@@ -139,5 +137,5 @@ lint.ignore = [
139
137
  "UP032",
140
138
  ]
141
139
  line-length = 120
142
- target-version = "py38"
140
+ target-version = "py39"
143
141
  exclude = ["doc", "example*.py", "tests/examples/*.py"]
@@ -4,8 +4,6 @@ from queue import Empty
4
4
  from queue import Queue
5
5
  from typing import TYPE_CHECKING
6
6
  from typing import Any
7
- from typing import Mapping
8
- from typing import Pattern
9
7
 
10
8
  from pytest_httpserver.httpserver import METHOD_ALL
11
9
  from pytest_httpserver.httpserver import UNDEFINED
@@ -16,6 +14,8 @@ from pytest_httpserver.httpserver import RequestHandlerBase
16
14
  from pytest_httpserver.httpserver import URIPattern
17
15
 
18
16
  if TYPE_CHECKING:
17
+ from collections.abc import Mapping
18
+ from re import Pattern
19
19
  from ssl import SSLContext
20
20
 
21
21
  from werkzeug import Request
@@ -9,20 +9,19 @@ import threading
9
9
  import time
10
10
  import urllib.parse
11
11
  from collections import defaultdict
12
+ from collections.abc import Iterable
13
+ from collections.abc import Mapping
14
+ from collections.abc import MutableMapping
12
15
  from contextlib import contextmanager
13
16
  from contextlib import suppress
14
17
  from copy import copy
15
18
  from enum import Enum
19
+ from re import Pattern
16
20
  from typing import TYPE_CHECKING
17
21
  from typing import Any
18
22
  from typing import Callable
19
23
  from typing import ClassVar
20
- from typing import Iterable
21
- from typing import Mapping
22
- from typing import MutableMapping
23
24
  from typing import Optional
24
- from typing import Pattern
25
- from typing import Tuple
26
25
  from typing import Union
27
26
 
28
27
  import werkzeug.http
@@ -34,13 +33,16 @@ from werkzeug.serving import make_server
34
33
 
35
34
  if TYPE_CHECKING:
36
35
  from ssl import SSLContext
36
+ from types import TracebackType
37
+
38
+ from werkzeug.serving import BaseWSGIServer
37
39
 
38
40
  URI_DEFAULT = ""
39
41
  METHOD_ALL = "__ALL"
40
42
 
41
43
  HEADERS_T = Union[
42
44
  Mapping[str, Union[str, Iterable[str]]],
43
- Iterable[Tuple[str, str]],
45
+ Iterable[tuple[str, str]],
44
46
  ]
45
47
 
46
48
  HVMATCHER_T = Callable[[str, Optional[str], str], bool]
@@ -113,11 +115,13 @@ class Waiting:
113
115
 
114
116
  @property
115
117
  def result(self) -> bool:
116
- return self._result
118
+ return bool(self._result)
117
119
 
118
120
  @property
119
121
  def elapsed_time(self) -> float:
120
122
  """Elapsed time in seconds"""
123
+ if self._stop is None:
124
+ raise TypeError("unsupported operand type(s) for -: 'NoneType' and 'float'")
121
125
  return self._stop - self._start
122
126
 
123
127
 
@@ -139,7 +143,7 @@ class HeaderValueMatcher:
139
143
  func = getattr(Authorization, "from_header", None)
140
144
  if func is None: # Werkzeug < 2.3.0
141
145
  func = werkzeug.http.parse_authorization_header # type: ignore[attr-defined]
142
- return func(actual) == func(expected)
146
+ return func(actual) == func(expected) # type: ignore
143
147
 
144
148
  @staticmethod
145
149
  def default_header_value_matcher(actual: str | None, expected: str) -> bool:
@@ -174,7 +178,7 @@ class QueryMatcher(abc.ABC):
174
178
  return values[0] == values[1]
175
179
 
176
180
  @abc.abstractmethod
177
- def get_comparing_values(self, request_query_string: bytes) -> tuple:
181
+ def get_comparing_values(self, request_query_string: bytes) -> tuple[Any, Any]:
178
182
  pass
179
183
 
180
184
 
@@ -195,10 +199,10 @@ class StringQueryMatcher(QueryMatcher):
195
199
 
196
200
  self.query_string = query_string
197
201
 
198
- def get_comparing_values(self, request_query_string: bytes) -> tuple:
202
+ def get_comparing_values(self, request_query_string: bytes) -> tuple[bytes, bytes]:
199
203
  if isinstance(self.query_string, str):
200
204
  query_string = self.query_string.encode()
201
- elif isinstance(self.query_string, bytes):
205
+ elif isinstance(self.query_string, bytes): # type: ignore
202
206
  query_string = self.query_string
203
207
  else:
204
208
  raise TypeError("query_string must be a string, or a bytes-like object")
@@ -211,7 +215,7 @@ class MappingQueryMatcher(QueryMatcher):
211
215
  Matches a query string to a dictionary or MultiDict specified
212
216
  """
213
217
 
214
- def __init__(self, query_dict: Mapping | MultiDict):
218
+ def __init__(self, query_dict: Mapping[str, str] | MultiDict[str, str]):
215
219
  """
216
220
  :param query_dict: if dictionary (Mapping) is specified, it will be used as a
217
221
  key-value mapping where both key and value should be string. If there are multiple
@@ -221,7 +225,7 @@ class MappingQueryMatcher(QueryMatcher):
221
225
  """
222
226
  self.query_dict = query_dict
223
227
 
224
- def get_comparing_values(self, request_query_string: bytes) -> tuple:
228
+ def get_comparing_values(self, request_query_string: bytes) -> tuple[Mapping[str, str], Mapping[str, str]]:
225
229
  query = MultiDict(urllib.parse.parse_qsl(request_query_string.decode("utf-8")))
226
230
  if isinstance(self.query_dict, MultiDict):
227
231
  return (query, self.query_dict)
@@ -241,14 +245,14 @@ class BooleanQueryMatcher(QueryMatcher):
241
245
  """
242
246
  self.result = result
243
247
 
244
- def get_comparing_values(self, request_query_string): # noqa: ARG002
248
+ def get_comparing_values(self, request_query_string: bytes): # noqa: ARG002
245
249
  if self.result:
246
250
  return (True, True)
247
251
  else:
248
252
  return (True, False)
249
253
 
250
254
 
251
- def _create_query_matcher(query_string: None | QueryMatcher | str | bytes | Mapping) -> QueryMatcher:
255
+ def _create_query_matcher(query_string: None | QueryMatcher | str | bytes | Mapping[str, str]) -> QueryMatcher:
252
256
  if isinstance(query_string, QueryMatcher):
253
257
  return query_string
254
258
 
@@ -312,7 +316,7 @@ class RequestMatcher:
312
316
  data: str | bytes | None = None,
313
317
  data_encoding: str = "utf-8",
314
318
  headers: Mapping[str, str] | None = None,
315
- query_string: None | QueryMatcher | str | bytes | Mapping = None,
319
+ query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None,
316
320
  header_value_matcher: HVMATCHER_T | None = None,
317
321
  json: Any = UNDEFINED,
318
322
  ):
@@ -410,7 +414,7 @@ class RequestMatcher:
410
414
 
411
415
  return json_received == self.json
412
416
 
413
- def difference(self, request: Request) -> list[tuple]:
417
+ def difference(self, request: Request) -> list[tuple[str, str, str | URIPattern]]:
414
418
  """
415
419
  Calculates the difference between the matcher and the request.
416
420
 
@@ -422,7 +426,7 @@ class RequestMatcher:
422
426
  matches the fields set in the matcher object.
423
427
  """
424
428
 
425
- retval: list[tuple] = []
429
+ retval: list[tuple[str, Any, Any]] = []
426
430
 
427
431
  if not self.match_uri(request):
428
432
  retval.append(("uri", request.path, self.uri))
@@ -433,8 +437,8 @@ class RequestMatcher:
433
437
  if not self.query_matcher.match(request.query_string):
434
438
  retval.append(("query_string", request.query_string, self.query_string))
435
439
 
436
- request_headers = {}
437
- expected_headers = {}
440
+ request_headers: dict[str, str | None] = {}
441
+ expected_headers: dict[str, str] = {}
438
442
  for key, value in self.headers.items():
439
443
  if not self.header_value_matcher(key, request.headers.get(key), value):
440
444
  request_headers[key] = request.headers.get(key)
@@ -467,7 +471,7 @@ class RequestHandlerBase(abc.ABC):
467
471
 
468
472
  def respond_with_json(
469
473
  self,
470
- response_json,
474
+ response_json: Any,
471
475
  status: int = 200,
472
476
  headers: Mapping[str, str] | None = None,
473
477
  content_type: str = "application/json",
@@ -578,7 +582,7 @@ class RequestHandler(RequestHandlerBase):
578
582
  return retval
579
583
 
580
584
 
581
- class RequestHandlerList(list):
585
+ class RequestHandlerList(list[RequestHandler]):
582
586
  """
583
587
  Represents a list of :py:class:`RequestHandler` objects.
584
588
 
@@ -638,9 +642,9 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
638
642
  """
639
643
  self.host = host
640
644
  self.port = port
641
- self.server = None
642
- self.server_thread = None
643
- self.assertions: list[str] = []
645
+ self.server: BaseWSGIServer | None = None
646
+ self.server_thread: threading.Thread | None = None
647
+ self.assertions: list[str | AssertionError] = []
644
648
  self.handler_errors: list[Exception] = []
645
649
  self.log: list[tuple[Request, Response]] = []
646
650
  self.ssl_context = ssl_context
@@ -727,7 +731,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
727
731
 
728
732
  This should not be called directly, but can be overridden to tailor it to your needs.
729
733
  """
730
-
734
+ assert self.server is not None
731
735
  self.server.serve_forever()
732
736
 
733
737
  def is_running(self) -> bool:
@@ -736,7 +740,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
736
740
  """
737
741
  return bool(self.server)
738
742
 
739
- def start(self):
743
+ def start(self) -> None:
740
744
  """
741
745
  Start the server in a thread.
742
746
 
@@ -755,9 +759,16 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
755
759
  if self.is_running():
756
760
  raise HTTPServerError("Server is already running")
757
761
 
762
+ app = Request.application(self.application)
763
+
758
764
  self.server = make_server(
759
- self.host, self.port, self.application, ssl_context=self.ssl_context, threaded=self.threaded
765
+ self.host,
766
+ self.port,
767
+ app,
768
+ ssl_context=self.ssl_context,
769
+ threaded=self.threaded,
760
770
  )
771
+
761
772
  self.port = self.server.port # Update port (needed if `port` was set to 0)
762
773
  self.server_thread = threading.Thread(target=self.thread_target)
763
774
  self.server_thread.start()
@@ -772,6 +783,8 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
772
783
  Only a running server can be stopped. If the sever is not running, :py:class`HTTPServerError`
773
784
  will be raised.
774
785
  """
786
+ assert self.server is not None
787
+ assert self.server_thread is not None
775
788
  if not self.is_running():
776
789
  raise HTTPServerError("Server is not running")
777
790
  self.server.shutdown()
@@ -779,7 +792,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
779
792
  self.server = None
780
793
  self.server_thread = None
781
794
 
782
- def add_assertion(self, obj):
795
+ def add_assertion(self, obj: str | AssertionError):
783
796
  """
784
797
  Add a new assertion
785
798
 
@@ -848,8 +861,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
848
861
  :return: the response object what the handler responded, or a response which contains the error
849
862
  """
850
863
 
851
- @Request.application # type: ignore
852
- def application(self, request: Request):
864
+ def application(self, request: Request) -> Response:
853
865
  """
854
866
  Entry point of werkzeug.
855
867
 
@@ -875,7 +887,12 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
875
887
  self.start()
876
888
  return self
877
889
 
878
- def __exit__(self, *args, **kwargs):
890
+ def __exit__(
891
+ self,
892
+ exc_type: type[BaseException] | None,
893
+ exc_value: BaseException | None,
894
+ traceback: TracebackType | None,
895
+ ):
879
896
  """
880
897
  Provide the context API
881
898
 
@@ -886,7 +903,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
886
903
  self.stop()
887
904
 
888
905
  @staticmethod
889
- def format_host(host):
906
+ def format_host(host: str):
890
907
  """
891
908
  Formats a hostname so it can be used in a URL.
892
909
  Notably, this adds brackets around IPV6 addresses when
@@ -929,8 +946,8 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
929
946
 
930
947
  def __init__(
931
948
  self,
932
- host=DEFAULT_LISTEN_HOST,
933
- port=DEFAULT_LISTEN_PORT,
949
+ host: str = DEFAULT_LISTEN_HOST,
950
+ port: int = DEFAULT_LISTEN_PORT,
934
951
  ssl_context: SSLContext | None = None,
935
952
  default_waiting_settings: WaitingSettings | None = None,
936
953
  *,
@@ -972,6 +989,22 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
972
989
  self.oneshot_handlers = RequestHandlerList()
973
990
  self.handlers = RequestHandlerList()
974
991
 
992
+ def expect(self, matcher: RequestMatcher, handler_type: HandlerType = HandlerType.PERMANENT) -> RequestHandler:
993
+ """
994
+ Create and register a request handler.
995
+
996
+ :param matcher: :py:class:`RequestMatcher` used to match requests.
997
+ :param handler_type: type of handler
998
+ """
999
+ request_handler = RequestHandler(matcher)
1000
+ if handler_type == HandlerType.PERMANENT:
1001
+ self.handlers.append(request_handler)
1002
+ elif handler_type == HandlerType.ONESHOT:
1003
+ self.oneshot_handlers.append(request_handler)
1004
+ elif handler_type == HandlerType.ORDERED:
1005
+ self.ordered_handlers.append(request_handler)
1006
+ return request_handler
1007
+
975
1008
  def expect_request(
976
1009
  self,
977
1010
  uri: str | URIPattern | Pattern[str],
@@ -979,7 +1012,7 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
979
1012
  data: str | bytes | None = None,
980
1013
  data_encoding: str = "utf-8",
981
1014
  headers: Mapping[str, str] | None = None,
982
- query_string: None | QueryMatcher | str | bytes | Mapping = None,
1015
+ query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None,
983
1016
  header_value_matcher: HVMATCHER_T | None = None,
984
1017
  handler_type: HandlerType = HandlerType.PERMANENT,
985
1018
  json: Any = UNDEFINED,
@@ -1062,7 +1095,7 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
1062
1095
  data: str | bytes | None = None,
1063
1096
  data_encoding: str = "utf-8",
1064
1097
  headers: Mapping[str, str] | None = None,
1065
- query_string: None | QueryMatcher | str | bytes | Mapping = None,
1098
+ query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None,
1066
1099
  header_value_matcher: HVMATCHER_T | None = None,
1067
1100
  json: Any = UNDEFINED,
1068
1101
  ) -> RequestHandler:
@@ -1117,7 +1150,7 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
1117
1150
  data: str | bytes | None = None,
1118
1151
  data_encoding: str = "utf-8",
1119
1152
  headers: Mapping[str, str] | None = None,
1120
- query_string: None | QueryMatcher | str | bytes | Mapping = None,
1153
+ query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None,
1121
1154
  header_value_matcher: HVMATCHER_T | None = None,
1122
1155
  json: Any = UNDEFINED,
1123
1156
  ) -> RequestHandler:
@@ -1175,13 +1208,13 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
1175
1208
  This method is primarily used when reporting errors.
1176
1209
  """
1177
1210
 
1178
- def format_handlers(handlers):
1211
+ def format_handlers(handlers: list[RequestHandler]):
1179
1212
  if handlers:
1180
1213
  return [" {!r}".format(handler.matcher) for handler in handlers]
1181
1214
  else:
1182
1215
  return [" none"]
1183
1216
 
1184
- lines = []
1217
+ lines: list[str] = []
1185
1218
  lines.append("Ordered matchers:")
1186
1219
  lines.extend(format_handlers(self.ordered_handlers))
1187
1220
  lines.append("")
@@ -61,7 +61,7 @@ def pytest_sessionfinish(session, exitstatus): # noqa: ARG001
61
61
  Plugin.SERVER.stop()
62
62
 
63
63
 
64
- @pytest.fixture()
64
+ @pytest.fixture
65
65
  def httpserver(make_httpserver):
66
66
  server = make_httpserver
67
67
  server.clear()
@@ -78,7 +78,7 @@ def make_httpserver_ipv4(httpserver_ssl_context):
78
78
  server.stop()
79
79
 
80
80
 
81
- @pytest.fixture()
81
+ @pytest.fixture
82
82
  def httpserver_ipv4(make_httpserver_ipv4):
83
83
  server = make_httpserver_ipv4
84
84
  server.clear()
@@ -95,7 +95,7 @@ def make_httpserver_ipv6(httpserver_ssl_context):
95
95
  server.stop()
96
96
 
97
97
 
98
- @pytest.fixture()
98
+ @pytest.fixture
99
99
  def httpserver_ipv6(make_httpserver_ipv6):
100
100
  server = make_httpserver_ipv6
101
101
  server.clear()
@@ -0,0 +1,37 @@
1
+ import requests
2
+ from werkzeug import Request
3
+
4
+ from pytest_httpserver import HTTPServer
5
+ from pytest_httpserver import RequestMatcher
6
+
7
+
8
+ class MyMatcher(RequestMatcher):
9
+ def match(self, request: Request) -> bool:
10
+ match = super().match(request)
11
+ if not match: # existing parameters didn't match -> return with False
12
+ return match
13
+
14
+ # match the json's "value" key: if it is an integer and it is an even
15
+ # number, it returns True
16
+ json = request.json
17
+ if isinstance(json, dict) and isinstance(json.get("value"), int):
18
+ return json["value"] % 2 == 0
19
+
20
+ return False
21
+
22
+
23
+ def test_custom_request_matcher(httpserver: HTTPServer):
24
+ httpserver.expect(MyMatcher("/foo")).respond_with_data("OK")
25
+
26
+ # with even number it matches the request
27
+ resp = requests.post(httpserver.url_for("/foo"), json={"value": 42})
28
+ resp.raise_for_status()
29
+ assert resp.text == "OK"
30
+
31
+ resp = requests.post(httpserver.url_for("/foo"), json={"value": 198})
32
+ resp.raise_for_status()
33
+ assert resp.text == "OK"
34
+
35
+ # with an odd number, it does not match the request
36
+ resp = requests.post(httpserver.url_for("/foo"), json={"value": 43})
37
+ assert resp.status_code == 500
@@ -40,7 +40,7 @@ def then_the_response_is_got_from(server_connection, response):
40
40
  assert server_connection.get(timeout=9).json() == response
41
41
 
42
42
 
43
- @pytest.fixture()
43
+ @pytest.fixture
44
44
  def httpserver():
45
45
  server = BlockingHTTPServer(timeout=1)
46
46
  server.start()
@@ -122,4 +122,4 @@ def test_raises_assertion_error_when_request_was_not_responded(httpserver: Block
122
122
 
123
123
 
124
124
  def test_repr(httpserver: BlockingHTTPServer):
125
- assert repr(httpserver) == f"<BlockingHTTPServer host=localhost port={httpserver.port}>"
125
+ assert repr(httpserver) == f"<BlockingHTTPServer host={httpserver.host} port={httpserver.port}>"
@@ -13,7 +13,7 @@ class Client:
13
13
  requests.get(self.url)
14
14
 
15
15
 
16
- @pytest.fixture()
16
+ @pytest.fixture
17
17
  def my_fixture():
18
18
  client = Client()
19
19
  yield client
@@ -47,7 +47,7 @@ def test_verify_assert_msg(httpserver: HTTPServer):
47
47
  "Path: /foo",
48
48
  "Method: GET",
49
49
  "Body: b''",
50
- f"Headers: Host: localhost:{httpserver.port}",
50
+ f"Headers: Host: {httpserver.host}:{httpserver.port}",
51
51
  "User-Agent: requests",
52
52
  "Accept-Encoding: gzip, deflate",
53
53
  "Accept: */*",
@@ -0,0 +1,11 @@
1
+ import requests
2
+
3
+ from pytest_httpserver import HTTPServer
4
+
5
+
6
+ def test_expect_method(httpserver: HTTPServer):
7
+ expected_response = "OK"
8
+ matcher = httpserver.create_matcher(uri="/test", method="POST")
9
+ httpserver.expect(matcher).respond_with_data(expected_response)
10
+ resp = requests.post(httpserver.url_for("/test"), json={"list": [1, 2, 3, 4]})
11
+ assert resp.text == expected_response
@@ -85,4 +85,4 @@ def test_all_ordered_missing(httpserver: HTTPServer):
85
85
 
86
86
 
87
87
  def test_repr(httpserver: HTTPServer):
88
- assert repr(httpserver) == f"<HTTPServer host=localhost port={httpserver.port}>"
88
+ assert repr(httpserver) == f"<HTTPServer host={httpserver.host} port={httpserver.port}>"
@@ -9,7 +9,7 @@ PORT_KEY = "PYTEST_HTTPSERVER_PORT"
9
9
  HOST_KEY = "PYTEST_HTTPSERVER_HOST"
10
10
 
11
11
 
12
- @pytest.fixture()
12
+ @pytest.fixture
13
13
  def tmpenv():
14
14
  old_vars = {}
15
15
  for key in (HOST_KEY, PORT_KEY):
@@ -8,10 +8,19 @@ import tarfile
8
8
  import zipfile
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
11
- from typing import Iterable
11
+ from typing import TYPE_CHECKING
12
12
 
13
13
  import pytest
14
- import toml
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Iterable
17
+
18
+ try:
19
+ import tomllib
20
+ except ImportError:
21
+ # Unfortunately mypy cannot handle this try/expect pattern, and "type: ignore"
22
+ # is the simplest work-around. See: https://github.com/python/mypy/issues/1153
23
+ import tomli as tomllib # type: ignore
15
24
 
16
25
  # TODO: skip if poetry is not available or add mark to test it explicitly
17
26
 
@@ -20,7 +29,7 @@ pytestmark = pytest.mark.release
20
29
 
21
30
  NAME = "pytest-httpserver"
22
31
  NAME_UNDERSCORE = NAME.replace("-", "_")
23
- PY_MAX_VERSION = (3, 12)
32
+ PY_MAX_VERSION = (3, 13)
24
33
 
25
34
 
26
35
  @pytest.fixture(scope="session")
@@ -31,8 +40,8 @@ def pyproject_path() -> Path:
31
40
  @pytest.fixture(scope="session")
32
41
  def pyproject(pyproject_path: Path):
33
42
  assert pyproject_path.is_file()
34
- with pyproject_path.open() as infile:
35
- pyproject = toml.load(infile)
43
+ with pyproject_path.open("rb") as infile:
44
+ pyproject = tomllib.load(infile)
36
45
  return pyproject
37
46
 
38
47
 
@@ -228,6 +237,7 @@ def test_sdist_contents(build: Build, version: str):
228
237
  "test_urimatch.py",
229
238
  "test_wait.py",
230
239
  "test_with_statement.py",
240
+ "test_matcher.py",
231
241
  },
232
242
  }
233
243
 
@@ -5,6 +5,8 @@ from os.path import join as pjoin
5
5
  import pytest
6
6
  import requests
7
7
 
8
+ from pytest_httpserver import HTTPServer
9
+
8
10
  pytestmark = pytest.mark.ssl
9
11
 
10
12
  test_dir = os.path.dirname(os.path.realpath(__file__))
@@ -24,18 +26,23 @@ def httpserver_ssl_context():
24
26
  return ssl.SSLContext(protocol)
25
27
 
26
28
 
27
- def test_ssl(httpserver):
29
+ def test_ssl(httpserver: HTTPServer):
28
30
  server_crt = pjoin(assets_dir, "server.crt")
29
31
  server_key = pjoin(assets_dir, "server.key")
30
32
  root_ca = pjoin(assets_dir, "rootCA.crt")
31
- context = httpserver.ssl_context
32
33
 
33
34
  assert (
34
- context is not None
35
+ httpserver.ssl_context is not None
35
36
  ), "SSLContext not set. The session was probably started with a test that did not define an SSLContext."
36
37
 
37
38
  httpserver.ssl_context.load_cert_chain(server_crt, server_key)
38
39
  httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"})
39
40
 
40
41
  assert httpserver.is_running()
41
- assert requests.get(httpserver.url_for("/foobar"), verify=root_ca).json() == {"foo": "bar"}
42
+
43
+ assert httpserver.url_for("/").startswith("https://")
44
+
45
+ # ensure we are using "localhost" and not "127.0.0.1" to pass cert verification
46
+ url = f"https://localhost:{httpserver.port}/foobar"
47
+
48
+ assert requests.get(url, verify=root_ca).json() == {"foo": "bar"}
@@ -1,7 +1,7 @@
1
1
  import http.client
2
2
  import threading
3
3
  import time
4
- from typing import Iterable
4
+ from collections.abc import Iterable
5
5
 
6
6
  import pytest
7
7
  from werkzeug import Request
@@ -10,7 +10,7 @@ from werkzeug import Response
10
10
  from pytest_httpserver import HTTPServer
11
11
 
12
12
 
13
- @pytest.fixture()
13
+ @pytest.fixture
14
14
  def threaded() -> Iterable[HTTPServer]:
15
15
  server = HTTPServer(threaded=True)
16
16
  server.start()
@@ -33,8 +33,6 @@ def test_threaded(threaded: HTTPServer):
33
33
 
34
34
  threaded.expect_request("/foo").respond_with_handler(handler)
35
35
 
36
- t_start = time.perf_counter()
37
-
38
36
  number_of_connections = 5
39
37
  conns = [http.client.HTTPConnection(threaded.host, threaded.port) for _ in range(number_of_connections)]
40
38
 
@@ -51,10 +49,4 @@ def test_threaded(threaded: HTTPServer):
51
49
  for conn in conns:
52
50
  conn.close()
53
51
 
54
- t_elapsed = time.perf_counter() - t_start
55
-
56
52
  assert len(thread_ids) == len(set(thread_ids)), "thread ids returned should be unique"
57
-
58
- assert (
59
- t_elapsed < number_of_connections * sleep_time * 0.9
60
- ), "elapsed time should be less than processing sequential requests"
@@ -4,19 +4,19 @@ This is package provides the main API for the pytest_httpserver package.
4
4
  """
5
5
 
6
6
  __all__ = [
7
+ "METHOD_ALL",
8
+ "URI_DEFAULT",
9
+ "BlockingHTTPServer",
10
+ "BlockingRequestHandler",
11
+ "Error",
7
12
  "HTTPServer",
8
13
  "HTTPServerError",
9
- "Error",
10
- "NoHandlerError",
11
- "WaitingSettings",
12
14
  "HeaderValueMatcher",
15
+ "NoHandlerError",
13
16
  "RequestHandler",
14
17
  "RequestMatcher",
15
18
  "URIPattern",
16
- "URI_DEFAULT",
17
- "METHOD_ALL",
18
- "BlockingHTTPServer",
19
- "BlockingRequestHandler",
19
+ "WaitingSettings",
20
20
  ]
21
21
 
22
22
  from .blocking_httpserver import BlockingHTTPServer