pytest_httpserver 1.1.1__tar.gz → 1.1.3__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 (84) hide show
  1. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/CHANGES.rst +28 -0
  2. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/PKG-INFO +2 -3
  3. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/conf.py +1 -1
  4. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/pyproject.toml +5 -5
  5. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/pytest_httpserver/blocking_httpserver.py +2 -2
  6. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/pytest_httpserver/httpserver.py +59 -41
  7. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_release.py +5 -1
  8. pytest_httpserver-1.1.3/tests/test_thread_type.py +21 -0
  9. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_threaded.py +1 -1
  10. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/CONTRIBUTION.md +0 -0
  11. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/LICENSE +0 -0
  12. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/README.md +0 -0
  13. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/Makefile +0 -0
  14. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/_static/.placeholder +0 -0
  15. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/api.rst +0 -0
  16. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/background.rst +0 -0
  17. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/changes.rst +0 -0
  18. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/fixtures.rst +0 -0
  19. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/guide.rst +0 -0
  20. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/howto.rst +0 -0
  21. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/index.rst +0 -0
  22. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/patch.py +0 -0
  23. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/tutorial.rst +0 -0
  24. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/doc/upgrade.rst +0 -0
  25. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/example.py +0 -0
  26. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/example_pytest.py +0 -0
  27. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/pytest_httpserver/__init__.py +0 -0
  28. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/pytest_httpserver/hooks.py +0 -0
  29. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/pytest_httpserver/py.typed +0 -0
  30. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/pytest_httpserver/pytest_plugin.py +0 -0
  31. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/Makefile +0 -0
  32. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/README +0 -0
  33. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/rootCA.cnf +0 -0
  34. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/rootCA.crt +0 -0
  35. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/rootCA.key +0 -0
  36. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/rootCA.srl +0 -0
  37. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/server.cnf +0 -0
  38. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/server.crt +0 -0
  39. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/server.csr +0 -0
  40. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/server.key +0 -0
  41. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/assets/v3.ext +0 -0
  42. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/conftest.py +0 -0
  43. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_example_blocking_httpserver.py +0 -0
  44. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_example_query_params1.py +0 -0
  45. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_example_query_params2.py +0 -0
  46. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_authorization_headers.py +0 -0
  47. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_case_insensitive_matcher.py +0 -0
  48. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_check.py +0 -0
  49. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_check_handler_errors.py +0 -0
  50. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_custom_handler.py +0 -0
  51. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_custom_hooks.py +0 -0
  52. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_custom_request_matcher.py +0 -0
  53. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_header_value_matcher.py +0 -0
  54. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_hooks.py +0 -0
  55. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_json_matcher.py +0 -0
  56. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_log_querying.py +0 -0
  57. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_query_params_dict.py +0 -0
  58. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_query_params_never_do_this.py +0 -0
  59. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_query_params_proper_use.py +0 -0
  60. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_regexp.py +0 -0
  61. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_timeout_requests.py +0 -0
  62. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_url_matcher.py +0 -0
  63. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/examples/test_howto_wait_success.py +0 -0
  64. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_blocking_httpserver.py +0 -0
  65. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_handler_errors.py +0 -0
  66. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_headers.py +0 -0
  67. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_hooks.py +0 -0
  68. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_ip_protocols.py +0 -0
  69. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_json_matcher.py +0 -0
  70. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_log_leak.py +0 -0
  71. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_log_querying.py +0 -0
  72. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_matcher.py +0 -0
  73. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_mixed.py +0 -0
  74. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_oneshot.py +0 -0
  75. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_ordered.py +0 -0
  76. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_parse_qs.py +0 -0
  77. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_permanent.py +0 -0
  78. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_port_changing.py +0 -0
  79. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_querymatcher.py +0 -0
  80. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_querystring.py +0 -0
  81. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_ssl.py +0 -0
  82. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_urimatch.py +0 -0
  83. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_wait.py +0 -0
  84. {pytest_httpserver-1.1.1 → pytest_httpserver-1.1.3}/tests/test_with_statement.py +0 -0
@@ -2,6 +2,34 @@
2
2
  Release Notes
3
3
  =============
4
4
 
5
+ .. _Release Notes_1.1.3:
6
+
7
+ 1.1.3
8
+ =====
9
+
10
+ .. _Release Notes_1.1.3_Bug Fixes:
11
+
12
+ Bug Fixes
13
+ ---------
14
+
15
+ - Run server threads with daemon flag, preventing shutdown issues.
16
+ `#411 <https://github.com/csernazs/pytest-httpserver/pull/411>`_
17
+
18
+
19
+ .. _Release Notes_1.1.2:
20
+
21
+ 1.1.2
22
+ =====
23
+
24
+ .. _Release Notes_1.1.2_Deprecation Notes:
25
+
26
+ Deprecation Notes
27
+ -----------------
28
+
29
+ - Python versions earlier than 3.9 have been deprecated in order to make the
30
+ code more type safe. Python 3.8 has reached EOL on 2024-10-07.
31
+
32
+
5
33
  .. _Release Notes_1.1.1:
6
34
 
7
35
  1.1.1
@@ -1,19 +1,18 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytest_httpserver
3
- Version: 1.1.1
3
+ Version: 1.1.3
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
@@ -68,7 +68,7 @@ author = "Zsolt Cserna"
68
68
  # built documents.
69
69
  #
70
70
  # The short X.Y version.
71
- version = "1.1.1"
71
+ version = "1.1.3"
72
72
  # The full version, including alpha/beta/rc tags.
73
73
  release = version
74
74
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pytest_httpserver"
3
- version = "1.1.1"
3
+ version = "1.1.3"
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
 
@@ -38,14 +38,14 @@ pytest_httpserver = "pytest_httpserver.pytest_plugin"
38
38
  optional = true
39
39
 
40
40
  [tool.poetry.group.develop.dependencies]
41
- pre-commit = ">=2.20,<4.0"
41
+ pre-commit = ">=2.20,<5.0"
42
42
  requests = "*"
43
43
  Sphinx = ">=5.1.1,<8.0.0"
44
44
  sphinx-rtd-theme = ">=1,<4"
45
45
  reno = "*"
46
46
  types-requests = "*"
47
47
  pytest = ">=7.1.3,<9.0.0"
48
- pytest-cov = ">=3,<6"
48
+ pytest-cov = ">=3,<7"
49
49
  coverage = ">=6.4.4,<8.0.0"
50
50
  tomli = { version = "*", markers = "python_version < '3.11'"}
51
51
  black = "*"
@@ -137,5 +137,5 @@ lint.ignore = [
137
137
  "UP032",
138
138
  ]
139
139
  line-length = 120
140
- target-version = "py38"
140
+ target-version = "py39"
141
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,11 +759,19 @@ 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
- self.server_thread = threading.Thread(target=self.thread_target)
773
+ # Explicitly make the new thread daemonic to avoid shutdown issues
774
+ self.server_thread = threading.Thread(target=self.thread_target, daemon=True)
763
775
  self.server_thread.start()
764
776
 
765
777
  def stop(self):
@@ -772,6 +784,8 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
772
784
  Only a running server can be stopped. If the sever is not running, :py:class`HTTPServerError`
773
785
  will be raised.
774
786
  """
787
+ assert self.server is not None
788
+ assert self.server_thread is not None
775
789
  if not self.is_running():
776
790
  raise HTTPServerError("Server is not running")
777
791
  self.server.shutdown()
@@ -779,7 +793,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
779
793
  self.server = None
780
794
  self.server_thread = None
781
795
 
782
- def add_assertion(self, obj):
796
+ def add_assertion(self, obj: str | AssertionError):
783
797
  """
784
798
  Add a new assertion
785
799
 
@@ -848,8 +862,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
848
862
  :return: the response object what the handler responded, or a response which contains the error
849
863
  """
850
864
 
851
- @Request.application # type: ignore
852
- def application(self, request: Request):
865
+ def application(self, request: Request) -> Response:
853
866
  """
854
867
  Entry point of werkzeug.
855
868
 
@@ -875,7 +888,12 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
875
888
  self.start()
876
889
  return self
877
890
 
878
- def __exit__(self, *args, **kwargs):
891
+ def __exit__(
892
+ self,
893
+ exc_type: type[BaseException] | None,
894
+ exc_value: BaseException | None,
895
+ traceback: TracebackType | None,
896
+ ):
879
897
  """
880
898
  Provide the context API
881
899
 
@@ -886,7 +904,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
886
904
  self.stop()
887
905
 
888
906
  @staticmethod
889
- def format_host(host):
907
+ def format_host(host: str):
890
908
  """
891
909
  Formats a hostname so it can be used in a URL.
892
910
  Notably, this adds brackets around IPV6 addresses when
@@ -929,8 +947,8 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
929
947
 
930
948
  def __init__(
931
949
  self,
932
- host=DEFAULT_LISTEN_HOST,
933
- port=DEFAULT_LISTEN_PORT,
950
+ host: str = DEFAULT_LISTEN_HOST,
951
+ port: int = DEFAULT_LISTEN_PORT,
934
952
  ssl_context: SSLContext | None = None,
935
953
  default_waiting_settings: WaitingSettings | None = None,
936
954
  *,
@@ -995,7 +1013,7 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
995
1013
  data: str | bytes | None = None,
996
1014
  data_encoding: str = "utf-8",
997
1015
  headers: Mapping[str, str] | None = None,
998
- query_string: None | QueryMatcher | str | bytes | Mapping = None,
1016
+ query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None,
999
1017
  header_value_matcher: HVMATCHER_T | None = None,
1000
1018
  handler_type: HandlerType = HandlerType.PERMANENT,
1001
1019
  json: Any = UNDEFINED,
@@ -1078,7 +1096,7 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
1078
1096
  data: str | bytes | None = None,
1079
1097
  data_encoding: str = "utf-8",
1080
1098
  headers: Mapping[str, str] | None = None,
1081
- query_string: None | QueryMatcher | str | bytes | Mapping = None,
1099
+ query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None,
1082
1100
  header_value_matcher: HVMATCHER_T | None = None,
1083
1101
  json: Any = UNDEFINED,
1084
1102
  ) -> RequestHandler:
@@ -1133,7 +1151,7 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
1133
1151
  data: str | bytes | None = None,
1134
1152
  data_encoding: str = "utf-8",
1135
1153
  headers: Mapping[str, str] | None = None,
1136
- query_string: None | QueryMatcher | str | bytes | Mapping = None,
1154
+ query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None,
1137
1155
  header_value_matcher: HVMATCHER_T | None = None,
1138
1156
  json: Any = UNDEFINED,
1139
1157
  ) -> RequestHandler:
@@ -1191,13 +1209,13 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
1191
1209
  This method is primarily used when reporting errors.
1192
1210
  """
1193
1211
 
1194
- def format_handlers(handlers):
1212
+ def format_handlers(handlers: list[RequestHandler]):
1195
1213
  if handlers:
1196
1214
  return [" {!r}".format(handler.matcher) for handler in handlers]
1197
1215
  else:
1198
1216
  return [" none"]
1199
1217
 
1200
- lines = []
1218
+ lines: list[str] = []
1201
1219
  lines.append("Ordered matchers:")
1202
1220
  lines.extend(format_handlers(self.ordered_handlers))
1203
1221
  lines.append("")
@@ -8,10 +8,13 @@ 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
14
 
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Iterable
17
+
15
18
  try:
16
19
  import tomllib
17
20
  except ImportError:
@@ -230,6 +233,7 @@ def test_sdist_contents(build: Build, version: str):
230
233
  "test_querystring.py",
231
234
  "test_release.py",
232
235
  "test_ssl.py",
236
+ "test_thread_type.py",
233
237
  "test_threaded.py",
234
238
  "test_urimatch.py",
235
239
  "test_wait.py",
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from typing import TYPE_CHECKING
5
+
6
+ import requests
7
+ from werkzeug import Response
8
+
9
+ if TYPE_CHECKING:
10
+ from werkzeug import Request
11
+
12
+ from pytest_httpserver import HTTPServer
13
+
14
+
15
+ def test_server_thread_is_daemon(httpserver: HTTPServer):
16
+ def handler(_request: Request):
17
+ return Response(f"{threading.current_thread().daemon}")
18
+
19
+ httpserver.expect_request("/foo").respond_with_handler(handler)
20
+
21
+ assert requests.get(httpserver.url_for("/foo")).text == "True"
@@ -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