Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pytest-httpserver for openSUSE:Factory checked in at 2024-03-01 23:35:49 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pytest-httpserver (Old) and /work/SRC/openSUSE:Factory/.python-pytest-httpserver.new.1770 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pytest-httpserver" Fri Mar 1 23:35:49 2024 rev:13 rq:1153853 version:1.0.10 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pytest-httpserver/python-pytest-httpserver.changes 2023-06-03 00:06:42.461806124 +0200 +++ /work/SRC/openSUSE:Factory/.python-pytest-httpserver.new.1770/python-pytest-httpserver.changes 2024-03-01 23:35:57.554226607 +0100 @@ -1,0 +2,14 @@ +Fri Mar 1 10:19:34 UTC 2024 - Dirk Müller <dmuel...@suse.com> + +- update to 1.0.10: + * When there's no handler for the request, add more details to + the response sent by the server about the request to help + debugging. + * Use ruff for linting. It includes some source code changes + which should not introduce functional changes, or API + changes. + * Add __repr__ to RequestHandler object so when it is compared + (eg. with the log attribute of the server) it will show the + matcher parameters. + +------------------------------------------------------------------- Old: ---- pytest_httpserver-1.0.8.tar.gz New: ---- pytest_httpserver-1.0.10.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pytest-httpserver.spec ++++++ --- /var/tmp/diff_new_pack.IFelks/_old 2024-03-01 23:35:58.046244401 +0100 +++ /var/tmp/diff_new_pack.IFelks/_new 2024-03-01 23:35:58.050244545 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-pytest-httpserver # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-pytest-httpserver -Version: 1.0.8 +Version: 1.0.10 Release: 0 Summary: A HTTP server for pytest License: MIT ++++++ pytest_httpserver-1.0.8.tar.gz -> pytest_httpserver-1.0.10.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/CHANGES.rst new/pytest_httpserver-1.0.10/CHANGES.rst --- old/pytest_httpserver-1.0.8/CHANGES.rst 2023-05-22 20:35:22.853064500 +0200 +++ new/pytest_httpserver-1.0.10/CHANGES.rst 2024-02-24 20:44:27.227664200 +0100 @@ -2,6 +2,43 @@ Release Notes ============= +.. _Release Notes_1.0.10: + +1.0.10 +====== + +.. _Release Notes_1.0.10_New Features: + +New Features +------------ + +- When there's no handler for the request, add more details to the response + sent by the server about the request to help debugging. + + +.. _Release Notes_1.0.10_Other Notes: + +Other Notes +----------- + +- Use ruff for linting. It includes some source code changes which should not + introduce functional changes, or API changes. + + +.. _Release Notes_1.0.9: + +1.0.9 +===== + +.. _Release Notes_1.0.9_New Features: + +New Features +------------ + +- Add ``__repr__`` to ``RequestHandler`` object so when it is compared (eg. with + the ``log`` attribute of the server) it will show the matcher parameters. + + .. _Release Notes_1.0.8: 1.0.8 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/PKG-INFO new/pytest_httpserver-1.0.10/PKG-INFO --- old/pytest_httpserver-1.0.8/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.10/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,12 +1,12 @@ Metadata-Version: 2.1 -Name: pytest-httpserver -Version: 1.0.8 +Name: pytest_httpserver +Version: 1.0.10 Summary: pytest-httpserver is a httpserver for pytest Home-page: https://github.com/csernazs/pytest-httpserver License: MIT Author: Zsolt Cserna Author-email: cserna.zs...@gmail.com -Requires-Python: >=3.8,<4.0 +Requires-Python: >=3.8 Classifier: Development Status :: 3 - Alpha Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers @@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: Werkzeug (>=2.0.0) Project-URL: Bug Tracker, https://github.com/csernazs/pytest-httpserver/issues @@ -26,10 +27,12 @@ [](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster) [](https://pytest-httpserver.readthedocs.io/en/latest/?badge=latest) - [](https://opensource.org/licenses/MIT) -[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=K6PU3AGBZW4QC&item_name=pytest-httpserver¤cy_code=EUR&source=url) +[](https://opensource.org/licenses/MIT) [](https://codecov.io/gh/csernazs/pytest-httpserver) [](https://github.com/psf/black) +[](https://github.com/astral-sh/ruff) +[](https://pepy.tech/project/pytest-httpserver) +[](https://github.com/pre-commit/pre-commit) ## pytest_httpserver @@ -143,12 +146,16 @@ ### Donation -If you want to donate to this project, you can find the donate button at the top -of the README. +Currently, this project is based heavily on werkzeug and pytest. -Currently, this project is based heavily on werkzeug. Werkzeug does all the heavy lifting -behind the scenes, parsing HTTP request and defining Request and Response objects, which -are currently transparent in the API. +Werkzeug does all the heavy lifting behind the scenes, parsing HTTP request and +defining Request and Response objects, which are currently transparent in the +API. -If you wish to donate, please consider donating to them: https://palletsprojects.com/donate +If you wish to donate to werkzeug: https://palletsprojects.com/donate + + +Pytest is the de-facto test library for python. + +If you wish to donate to pytest: https://opencollective.com/pytest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/README.md new/pytest_httpserver-1.0.10/README.md --- old/pytest_httpserver-1.0.8/README.md 2023-01-18 23:01:30.858852400 +0100 +++ new/pytest_httpserver-1.0.10/README.md 2024-02-24 20:33:46.159088900 +0100 @@ -1,9 +1,11 @@ [](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster) [](https://pytest-httpserver.readthedocs.io/en/latest/?badge=latest) - [](https://opensource.org/licenses/MIT) -[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=K6PU3AGBZW4QC&item_name=pytest-httpserver¤cy_code=EUR&source=url) +[](https://opensource.org/licenses/MIT) [](https://codecov.io/gh/csernazs/pytest-httpserver) [](https://github.com/psf/black) +[](https://github.com/astral-sh/ruff) +[](https://pepy.tech/project/pytest-httpserver) +[](https://github.com/pre-commit/pre-commit) ## pytest_httpserver @@ -117,11 +119,15 @@ ### Donation -If you want to donate to this project, you can find the donate button at the top -of the README. +Currently, this project is based heavily on werkzeug and pytest. -Currently, this project is based heavily on werkzeug. Werkzeug does all the heavy lifting -behind the scenes, parsing HTTP request and defining Request and Response objects, which -are currently transparent in the API. +Werkzeug does all the heavy lifting behind the scenes, parsing HTTP request and +defining Request and Response objects, which are currently transparent in the +API. -If you wish to donate, please consider donating to them: https://palletsprojects.com/donate +If you wish to donate to werkzeug: https://palletsprojects.com/donate + + +Pytest is the de-facto test library for python. + +If you wish to donate to pytest: https://opencollective.com/pytest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/doc/conf.py new/pytest_httpserver-1.0.10/doc/conf.py --- old/pytest_httpserver-1.0.8/doc/conf.py 2023-05-22 20:35:22.853064500 +0200 +++ new/pytest_httpserver-1.0.10/doc/conf.py 2024-02-24 20:44:27.227664200 +0100 @@ -66,7 +66,7 @@ # built documents. # # The short X.Y version. -version = "1.0.8" +version = "1.0.10" # The full version, including alpha/beta/rc tags. release = version diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/doc/tutorial.rst new/pytest_httpserver-1.0.10/doc/tutorial.rst --- old/pytest_httpserver-1.0.8/doc/tutorial.rst 2023-05-16 20:58:37.763110000 +0200 +++ new/pytest_httpserver-1.0.10/doc/tutorial.rst 2024-02-17 17:55:07.351395600 +0100 @@ -39,6 +39,12 @@ it must respond with the provided json. The library accepts here any python object which is json serializable. Here, a dict is provided. +.. note:: + + It is important to specify what response to be sent back to the client + otherwise *pytest-httpserver* will error with ``Matching request handler + found but no response defined`` message on an incoming request. + In the next line, an http request is sent with the *requests* library: .. code:: python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/pyproject.toml new/pytest_httpserver-1.0.10/pyproject.toml --- old/pytest_httpserver-1.0.8/pyproject.toml 2023-05-22 20:35:22.853064500 +0200 +++ new/pytest_httpserver-1.0.10/pyproject.toml 2024-02-24 20:44:27.227664200 +0100 @@ -1,6 +1,6 @@ [tool.poetry] name = "pytest_httpserver" -version = "1.0.8" +version = "1.0.10" description = "pytest-httpserver is a httpserver for pytest" authors = ["Zsolt Cserna <cserna.zs...@gmail.com>"] license = "MIT" @@ -24,7 +24,7 @@ ] [tool.poetry.dependencies] -python = ">=3.8,<4.0" +python = ">=3.8" Werkzeug = ">= 2.0.0" @@ -51,6 +51,7 @@ types-toml = "^0.10.8" toml = "^0.10.2" black = "^23.1.0" +ruff = "^0.2.1" [tool.poetry.group.doc] @@ -88,3 +89,55 @@ [tool.mypy] files = ["pytest_httpserver", "scripts", "tests", "doc"] implicit_reexport = false + + +[tool.black] +line-length = 120 +safe = true + +[tool.ruff] +lint.select = ["ALL"] +lint.ignore = [ + "I", + "D", + + "ANN", + "ARG005", + "B011", + "B904", + "C408", + "C901", + "COM812", + "EM101", + "EM103", + "FBT002", + "FIX002", + "INP001", + "PGH003", + "PLR0912", + "PLR0913", + "PLR2004", + "PLW2901", + "PT004", + "PT012", + "PT013", + "PTH118", + "PTH120", + "RET504", + "RET505", + "RET506", + "RUF005", + "S101", + "S113", + "S603", + "S607", + "SIM108", + "T201", + "TD002", + "TD003", + "TRY003", + "UP032", +] +line-length = 120 +target-version = "py38" +exclude = ["doc", "example*.py", "tests/examples/*.py"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/pytest_httpserver/blocking_httpserver.py new/pytest_httpserver-1.0.10/pytest_httpserver/blocking_httpserver.py --- old/pytest_httpserver-1.0.8/pytest_httpserver/blocking_httpserver.py 2023-02-03 08:55:52.829130600 +0100 +++ new/pytest_httpserver-1.0.10/pytest_httpserver/blocking_httpserver.py 2024-02-17 17:55:07.352395800 +0100 @@ -1,15 +1,11 @@ +from __future__ import annotations + from queue import Empty from queue import Queue -from ssl import SSLContext +from typing import TYPE_CHECKING from typing import Any -from typing import Dict from typing import Mapping -from typing import Optional from typing import Pattern -from typing import Union - -from werkzeug.wrappers import Request -from werkzeug.wrappers import Response from pytest_httpserver.httpserver import METHOD_ALL from pytest_httpserver.httpserver import UNDEFINED @@ -19,6 +15,12 @@ from pytest_httpserver.httpserver import RequestHandlerBase from pytest_httpserver.httpserver import URIPattern +if TYPE_CHECKING: + from ssl import SSLContext + + from werkzeug.wrappers import Request + from werkzeug.wrappers import Response + class BlockingRequestHandler(RequestHandlerBase): """ @@ -59,23 +61,23 @@ self, host=DEFAULT_LISTEN_HOST, port=DEFAULT_LISTEN_PORT, - ssl_context: Optional[SSLContext] = None, + ssl_context: SSLContext | None = None, timeout: int = 30, ): super().__init__(host, port, ssl_context) self.timeout = timeout self.request_queue: Queue[Request] = Queue() - self.request_handlers: Dict[Request, Queue[BlockingRequestHandler]] = {} + self.request_handlers: dict[Request, Queue[BlockingRequestHandler]] = {} def assert_request( self, - uri: Union[str, URIPattern, Pattern[str]], + uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, - data: Union[str, bytes, None] = None, + data: str | bytes | None = None, data_encoding: str = "utf-8", - headers: Optional[Mapping[str, str]] = None, - query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HeaderValueMatcher] = None, + headers: Mapping[str, str] | None = None, + query_string: None | QueryMatcher | str | bytes | Mapping = None, + header_value_matcher: HeaderValueMatcher | None = None, json: Any = UNDEFINED, timeout: int = 30, ) -> BlockingRequestHandler: @@ -127,7 +129,7 @@ try: request = self.request_queue.get(timeout=timeout) except Empty: - raise AssertionError(f"Waiting for request {matcher} timed out") + raise AssertionError(f"Waiting for request {matcher} timed out") # noqa: EM102 diff = matcher.difference(request) @@ -137,7 +139,7 @@ if diff: request_handler.respond_with_response(self.respond_nohandler(request)) - raise AssertionError(f"Request {matcher} does not match: {diff}") + raise AssertionError(f"Request {matcher} does not match: {diff}") # noqa: EM102 return request_handler diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/pytest_httpserver/httpserver.py new/pytest_httpserver-1.0.10/pytest_httpserver/httpserver.py --- old/pytest_httpserver-1.0.8/pytest_httpserver/httpserver.py 2023-05-16 20:58:37.764109800 +0200 +++ new/pytest_httpserver-1.0.10/pytest_httpserver/httpserver.py 2024-02-20 08:59:32.854914200 +0100 @@ -1,3 +1,5 @@ +from __future__ import annotations + import abc import ipaddress import json @@ -11,11 +13,11 @@ from contextlib import suppress from copy import copy from enum import Enum -from ssl import SSLContext +from typing import TYPE_CHECKING from typing import Any from typing import Callable +from typing import ClassVar from typing import Iterable -from typing import List from typing import Mapping from typing import MutableMapping from typing import Optional @@ -30,6 +32,9 @@ from werkzeug.wrappers import Request from werkzeug.wrappers import Response +if TYPE_CHECKING: + from ssl import SSLContext + URI_DEFAULT = "" METHOD_ALL = "__ALL" @@ -54,32 +59,24 @@ Base class for all exception defined in this package. """ - pass - class NoHandlerError(Error): """ Raised when a :py:class:`RequestHandler` has no registered method to serve the request. """ - pass - class HTTPServerError(Error): """ Raised when there's a problem with HTTP server. """ - pass - class NoMethodFoundForMatchingHeaderValueError(Error): """ Raised when a :py:class:`HeaderValueMatcher` has no registered method to match the header value. """ - pass - class WaitingSettings: """Class for providing default settings and storing them in HTTPServer @@ -89,7 +86,12 @@ :param timeout: time (in seconds) until time is out """ - def __init__(self, raise_assertions: bool = True, stop_on_nohandler: bool = True, timeout: float = 5): + def __init__( + self, + raise_assertions: bool = True, # noqa: FBT001 + stop_on_nohandler: bool = True, # noqa: FBT001 + timeout: float = 5, + ): self.raise_assertions = raise_assertions self.stop_on_nohandler = stop_on_nohandler self.timeout = timeout @@ -105,7 +107,7 @@ self._start = time.monotonic() self._stop = None - def complete(self, result: bool): + def complete(self, result: bool): # noqa: FBT001 self._result = result self._stop = time.monotonic() @@ -127,23 +129,23 @@ and return whether they are equal as bool. """ - DEFAULT_MATCHERS: MutableMapping[str, Callable[[Optional[str], str], bool]] = {} + DEFAULT_MATCHERS: ClassVar[MutableMapping[str, Callable[[str | None, str], bool]]] = {} - def __init__(self, matchers: Optional[Mapping[str, Callable[[Optional[str], str], bool]]] = None): + def __init__(self, matchers: Mapping[str, Callable[[str | None, str], bool]] | None = None): self.matchers = self.DEFAULT_MATCHERS if matchers is None else matchers @staticmethod - def authorization_header_value_matcher(actual: Optional[str], expected: str) -> bool: - callable = getattr(Authorization, "from_header", None) - if callable is None: # Werkzeug < 2.3.0 - callable = werkzeug.http.parse_authorization_header - return callable(actual) == callable(expected) + def authorization_header_value_matcher(actual: str | None, expected: str) -> bool: + func = getattr(Authorization, "from_header", None) + if func is None: # Werkzeug < 2.3.0 + func = werkzeug.http.parse_authorization_header # type: ignore[attr-defined] + return func(actual) == func(expected) @staticmethod - def default_header_value_matcher(actual: Optional[str], expected: str) -> bool: + def default_header_value_matcher(actual: str | None, expected: str) -> bool: return actual == expected - def __call__(self, header_name: str, actual: Optional[str], expected: str) -> bool: + def __call__(self, header_name: str, actual: str | None, expected: str) -> bool: try: matcher = self.matchers[header_name] except KeyError: @@ -181,7 +183,7 @@ Matches a query for a string or bytes specified """ - def __init__(self, query_string: Union[bytes, str]): + def __init__(self, query_string: bytes | str): """ :param query_string: the query string will be compared to this string or bytes. If string is specified, it will be encoded by the encode() method. @@ -209,7 +211,7 @@ Matches a query string to a dictionary or MultiDict specified """ - def __init__(self, query_dict: Union[Mapping, MultiDict]): + def __init__(self, query_dict: Mapping | MultiDict): """ :param query_dict: if dictionary (Mapping) is specified, it will be used as a key-value mapping where both key and value should be string. If there are multiple @@ -232,26 +234,26 @@ Matches the query depending on the boolean value """ - def __init__(self, result: bool): + def __init__(self, result: bool): # noqa: FBT001 """ :param result: if this parameter is true, the query match will be always successful. Otherwise, no query match will be successful. """ self.result = result - def get_comparing_values(self, request_query_string): + def get_comparing_values(self, request_query_string): # noqa: ARG002 if self.result: return (True, True) else: return (True, False) -def _create_query_matcher(query_string: Union[None, QueryMatcher, str, bytes, Mapping]) -> QueryMatcher: +def _create_query_matcher(query_string: None | QueryMatcher | str | bytes | Mapping) -> QueryMatcher: if isinstance(query_string, QueryMatcher): return query_string if query_string is None: - return BooleanQueryMatcher(True) + return BooleanQueryMatcher(result=True) if isinstance(query_string, (str, bytes)): return StringQueryMatcher(query_string) @@ -272,7 +274,6 @@ with "/" and does not contain the query part. :return: True if there's a match, False otherwise """ - pass class RequestMatcher: @@ -306,13 +307,13 @@ def __init__( self, - uri: Union[str, URIPattern, Pattern[str]], + uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, - data: Union[str, bytes, None] = None, + data: str | bytes | None = None, data_encoding: str = "utf-8", - headers: Optional[Mapping[str, str]] = None, - query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HVMATCHER_T] = None, + headers: Mapping[str, str] | None = None, + query_string: None | QueryMatcher | str | bytes | Mapping = None, + header_value_matcher: HVMATCHER_T | None = None, json: Any = UNDEFINED, ): if json is not UNDEFINED and data is not None: @@ -383,7 +384,7 @@ # # also, python will raise TypeError when self.uri is a conflicting type - return self.uri == URI_DEFAULT or path == self.uri + return self.uri in (URI_DEFAULT, path) def match_json(self, request: Request) -> bool: """ @@ -409,7 +410,7 @@ return json_received == self.json - def difference(self, request: Request) -> List[Tuple]: + def difference(self, request: Request) -> list[tuple]: """ Calculates the difference between the matcher and the request. @@ -421,12 +422,12 @@ matches the fields set in the matcher object. """ - retval: List[Tuple] = [] + retval: list[tuple] = [] if not self.match_uri(request): retval.append(("uri", request.path, self.uri)) - if self.method != METHOD_ALL and self.method != request.method: + if self.method not in (METHOD_ALL, request.method): retval.append(("method", request.method, self.method)) if not self.query_matcher.match(request.query_string): @@ -468,7 +469,7 @@ self, response_json, status: int = 200, - headers: Optional[Mapping[str, str]] = None, + headers: Mapping[str, str] | None = None, content_type: str = "application/json", ): """ @@ -485,11 +486,11 @@ def respond_with_data( self, - response_data: Union[str, bytes] = "", + response_data: str | bytes = "", status: int = 200, - headers: Optional[HEADERS_T] = None, - mimetype: Optional[str] = None, - content_type: Optional[str] = None, + headers: HEADERS_T | None = None, + mimetype: str | None = None, + content_type: str | None = None, ): """ Prepares a response with raw data. @@ -514,8 +515,6 @@ :param response: the response object which will be responded """ - pass - class RequestHandler(RequestHandlerBase): """ @@ -529,7 +528,7 @@ def __init__(self, matcher: RequestMatcher): self.matcher = matcher - self.request_handler: Optional[Callable[[Request], Response]] = None + self.request_handler: Callable[[Request], Response] | None = None def respond(self, request: Request) -> Response: """ @@ -560,6 +559,15 @@ def respond_with_response(self, response: Response): self.request_handler = lambda request: response + def __repr__(self) -> str: + class_name = self.__class__.__name__ + retval = ( + f"<{class_name} uri={self.matcher.uri!r} method={self.matcher.method!r} " + f"query_string={self.matcher.query_string!r} headers={self.matcher.headers!r} data={self.matcher.data!r} " + f"json={self.matcher.json!r}>" + ) + return retval + class RequestHandlerList(list): """ @@ -567,7 +575,7 @@ """ - def match(self, request: Request) -> Optional[RequestHandler]: + def match(self, request: Request) -> RequestHandler | None: """ Returns the first request handler which matches the specified request. Otherwise, it returns `None`. """ @@ -610,7 +618,7 @@ self, host: str, port: int, - ssl_context: Optional[SSLContext] = None, + ssl_context: SSLContext | None = None, ): """ Initializes the instance. @@ -620,9 +628,9 @@ self.port = port self.server = None self.server_thread = None - self.assertions: List[str] = [] - self.handler_errors: List[Exception] = [] - self.log: List[Tuple[Request, Response]] = [] + self.assertions: list[str] = [] + self.handler_errors: list[Exception] = [] + self.log: list[tuple[Request, Response]] = [] self.ssl_context = ssl_context self.no_handler_status_code = 500 @@ -812,9 +820,9 @@ As the result, there's an assertion added (which can be raised by :py:meth:`check_assertions`). """ - text = "No handler found for request {!r}.\n".format(request) + text = "No handler found for request {!r} with data {!r}.".format(request, request.data) self.add_assertion(text + extra_message) - return Response("No handler found for this request", self.no_handler_status_code) + return Response(text + extra_message, self.no_handler_status_code) @abc.abstractmethod def dispatch(self, request: Request) -> Response: @@ -824,7 +832,6 @@ :param request: the request object from the werkzeug library :return: the response object what the handler responded, or a response which contains the error """ - pass @Request.application # type: ignore def application(self, request: Request): @@ -907,15 +914,15 @@ self, host=DEFAULT_LISTEN_HOST, port=DEFAULT_LISTEN_PORT, - ssl_context: Optional[SSLContext] = None, - default_waiting_settings: Optional[WaitingSettings] = None, + ssl_context: SSLContext | None = None, + default_waiting_settings: WaitingSettings | None = None, ): """ Initializes the instance. """ super().__init__(host, port, ssl_context) - self.ordered_handlers: List[RequestHandler] = [] + self.ordered_handlers: list[RequestHandler] = [] self.oneshot_handlers = RequestHandlerList() self.handlers = RequestHandlerList() self.permanently_failed = False @@ -948,13 +955,13 @@ def expect_request( self, - uri: Union[str, URIPattern, Pattern[str]], + uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, - data: Union[str, bytes, None] = None, + data: str | bytes | None = None, data_encoding: str = "utf-8", - headers: Optional[Mapping[str, str]] = None, - query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HVMATCHER_T] = None, + headers: Mapping[str, str] | None = None, + query_string: None | QueryMatcher | str | bytes | Mapping = None, + header_value_matcher: HVMATCHER_T | None = None, handler_type: HandlerType = HandlerType.PERMANENT, json: Any = UNDEFINED, ) -> RequestHandler: @@ -971,7 +978,15 @@ If `handler_type` is `HandlerType.ORDERED` an ordered request handler is created. Comparing to oneshot handler, ordered handler also determines the order of the requests to be served. For example if there are two ordered handlers registered, the first request must hit the first handler, and the second request must hit the second - one, and not vice versa. If one or more ordered handler defined, those must be exhausted first. + one, and not vice versa. If one or more ordered handler defined, those + must be exhausted first. + + .. note:: + + Once this method is called, the response should also be specified by + calling one of the respond methods of the returned + :py:class:`RequestHandler` object, otherwise + :py:class:`NoHandlerError` will be raised on an incoming request. :param uri: URI of the request. This must be an absolute path starting with ``/``, a @@ -1023,13 +1038,13 @@ def expect_oneshot_request( self, - uri: Union[str, URIPattern, Pattern[str]], + uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, - data: Union[str, bytes, None] = None, + data: str | bytes | None = None, data_encoding: str = "utf-8", - headers: Optional[Mapping[str, str]] = None, - query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HVMATCHER_T] = None, + headers: Mapping[str, str] | None = None, + query_string: None | QueryMatcher | str | bytes | Mapping = None, + header_value_matcher: HVMATCHER_T | None = None, json: Any = UNDEFINED, ) -> RequestHandler: """ @@ -1078,13 +1093,13 @@ def expect_ordered_request( self, - uri: Union[str, URIPattern, Pattern[str]], + uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, - data: Union[str, bytes, None] = None, + data: str | bytes | None = None, data_encoding: str = "utf-8", - headers: Optional[Mapping[str, str]] = None, - query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HVMATCHER_T] = None, + headers: Mapping[str, str] | None = None, + query_string: None | QueryMatcher | str | bytes | Mapping = None, + header_value_matcher: HVMATCHER_T | None = None, json: Any = UNDEFINED, ) -> RequestHandler: """ @@ -1168,7 +1183,7 @@ """ if self._waiting_settings.stop_on_nohandler: - self._set_waiting_result(False) + self._set_waiting_result(value=False) return super().respond_nohandler(request, self.format_matchers() + extra_message) @@ -1246,7 +1261,7 @@ return response - def _set_waiting_result(self, value: bool) -> None: + def _set_waiting_result(self, value: bool) -> None: # noqa: FBT001 """Set waiting_result Setting is implemented as putting value to queue without waiting. If queue is full we simply ignore the @@ -1257,14 +1272,14 @@ def _update_waiting_result(self) -> None: if not self.oneshot_handlers and not self.ordered_handlers: - self._set_waiting_result(True) + self._set_waiting_result(value=True) @contextmanager def wait( self, - raise_assertions: Optional[bool] = None, - stop_on_nohandler: Optional[bool] = None, - timeout: Optional[float] = None, + raise_assertions: bool | None = None, + stop_on_nohandler: bool | None = None, + timeout: float | None = None, ): """Context manager to wait until the first of following event occurs: all ordered and oneshot handlers were executed, unexpected request was received (if `stop_on_nohandler` is set to `True`), or time was out @@ -1314,7 +1329,7 @@ waiting.complete(result=False) if self._waiting_settings.raise_assertions: raise AssertionError( - "Wait timeout occurred, but some handlers left:\n" "{}".format(self.format_matchers()) + "Wait timeout occurred, but some handlers left:\n{}".format(self.format_matchers()) ) if self._waiting_settings.raise_assertions and not waiting.result: self.check_assertions() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/pytest_httpserver/pytest_plugin.py new/pytest_httpserver-1.0.10/pytest_httpserver/pytest_plugin.py --- old/pytest_httpserver-1.0.8/pytest_httpserver/pytest_plugin.py 2023-05-16 20:58:37.764109800 +0200 +++ new/pytest_httpserver-1.0.10/pytest_httpserver/pytest_plugin.py 2024-02-17 17:55:07.352395800 +0100 @@ -54,14 +54,14 @@ server.stop() -def pytest_sessionfinish(session, exitstatus): # pylint: disable=unused-argument +def pytest_sessionfinish(session, exitstatus): # noqa: ARG001 if Plugin.SERVER is not None: Plugin.SERVER.clear() if Plugin.SERVER.is_running(): Plugin.SERVER.stop() -@pytest.fixture +@pytest.fixture() def httpserver(make_httpserver): server = make_httpserver yield server @@ -78,7 +78,7 @@ server.stop() -@pytest.fixture +@pytest.fixture() def httpserver_ipv4(make_httpserver_ipv4): server = make_httpserver_ipv4 yield server @@ -95,7 +95,7 @@ server.stop() -@pytest.fixture +@pytest.fixture() def httpserver_ipv6(make_httpserver_ipv6): server = make_httpserver_ipv6 yield server diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/setup.py new/pytest_httpserver-1.0.10/setup.py --- old/pytest_httpserver-1.0.8/setup.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.10/setup.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -from setuptools import setup - -packages = \ -['pytest_httpserver'] - -package_data = \ -{'': ['*']} - -install_requires = \ -['Werkzeug>=2.0.0'] - -entry_points = \ -{'pytest11': ['pytest_httpserver = pytest_httpserver.pytest_plugin']} - -setup_kwargs = { - 'name': 'pytest-httpserver', - 'version': '1.0.8', - 'description': 'pytest-httpserver is a httpserver for pytest', - 'long_description': '[](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster)\n[](https://pytest-httpserver.readthedocs.io/en/latest/?badge=latest)\n [](https://opensource.org/licenses/MIT)\n[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=K6PU3AGBZW4QC&item_name=pytest-httpserver¤cy_code=EUR&source=url)\n[](https://codecov.io/gh/csernazs/pytest-httpserver)\n[](https://github.com/psf/black)\n\n## pytest_httpserve r\n\nHTTP server for pytest\n\n\n### Nutshell\n\nThis library is designed to help to test http clients without contacting the real http server.\nIn other words, it is a fake http server which is accessible via localhost can be started with\nthe pre-defined expected http requests and their responses.\n\n### Example\n\n#### Handling a simple GET request\n```python\ndef test_my_client(\n httpserver,\n): # httpserver is a pytest fixture which starts the server\n # set up the server to serve /foobar with the json\n httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"})\n # check that the request is served\n assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"}\n```\n\n#### Handing a POST request with an expected json body\n```python\ndef test_json_request(\n httpserver,\n): # httpserver is a pytest fixture which starts the server\n # set up the server to serve /foobar with the json\n httpserver.expect_request(\n "/foo bar", method="POST", json={"id": 12, "name": "foo"}\n ).respond_with_json({"foo": "bar"})\n # check that the request is served\n assert requests.post(\n httpserver.url_for("/foobar"), json={"id": 12, "name": "foo"}\n ).json() == {"foo": "bar"}\n```\n\n\nYou can also use the library without pytest. There\'s a with statement to ensure that the server is stopped.\n\n\n```python\nwith HTTPServer() as httpserver:\n # set up the server to serve /foobar with the json\n httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"})\n # check that the request is served\n print(requests.get(httpserver.url_for("/foobar")).json())\n```\n\n### Documentation\n\nPlease find the API documentation at https://pytest-httpserver.readthedocs.io/en/latest/.\n\n### Features\n\nYou can set up a dozen of expectations for the requests, and also what response should be sent by the server to the client.\n\n\n#### Requests\n\nThere are three different types:\n\n- **permane nt**: this will be always served when there\'s match for this request, you can make as many HTTP requests as you want\n- **oneshot**: this will be served only once when there\'s a match for this request, you can only make 1 HTTP request\n- **ordered**: same as oneshot but the order must be strictly matched to the order of setting up\n\nYou can also fine-tune the expected request. The following can be specified:\n\n- URI (this is a must)\n- HTTP method\n- headers\n- query string\n- data (HTTP body of the request)\n- JSON (HTTP body loaded as JSON)\n\n\n#### Responses\n\nOnce you have the expectations for the request set up, you should also define the response you want to send back.\nThe following is supported currently:\n\n- respond arbitrary data (string or bytearray)\n- respond a json (a python dict converted in-place to json)\n- respond a Response object of werkzeug\n- use your own function\n\nSimilar to requests, you can fine-tune what response you want to send:\n\n- HTTP status\ n- headers\n- data\n\n\n#### Behave support\n\nUsing the `BlockingHTTPServer` class, the assertion for a request and the\nresponse can be performed in real order. For more info, see the\n[test](tests/test_blocking_httpserver.py), the\n[howto](https://pytest-httpserver.readthedocs.io/en/latest/howto.html#running-httpserver-in-blocking-mode)\nand the [API\ndocumentation](https://pytest-httpserver.readthedocs.io/en/latest/api.html#blockinghttpserver).\n\n\n### Missing features\n* HTTP/2\n* Keepalive\n* ~~TLS~~\n\n### Donation\n\nIf you want to donate to this project, you can find the donate button at the top\nof the README.\n\nCurrently, this project is based heavily on werkzeug. Werkzeug does all the heavy lifting\nbehind the scenes, parsing HTTP request and defining Request and Response objects, which\nare currently transparent in the API.\n\nIf you wish to donate, please consider donating to them: https://palletsprojects.com/donate\n', - 'author': 'Zsolt Cserna', - 'author_email': 'cserna.zs...@gmail.com', - 'maintainer': 'None', - 'maintainer_email': 'None', - 'url': 'https://github.com/csernazs/pytest-httpserver', - 'packages': packages, - 'package_data': package_data, - 'install_requires': install_requires, - 'entry_points': entry_points, - 'python_requires': '>=3.8,<4.0', -} - - -setup(**setup_kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/examples/test_example_blocking_httpserver.py new/pytest_httpserver-1.0.10/tests/examples/test_example_blocking_httpserver.py --- old/pytest_httpserver-1.0.8/tests/examples/test_example_blocking_httpserver.py 2023-05-16 20:58:37.764109800 +0200 +++ new/pytest_httpserver-1.0.10/tests/examples/test_example_blocking_httpserver.py 2024-02-17 17:55:07.352395800 +0100 @@ -16,7 +16,6 @@ yield server - server.clear() if server.is_running(): server.stop() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_blocking_httpserver.py new/pytest_httpserver-1.0.10/tests/test_blocking_httpserver.py --- old/pytest_httpserver-1.0.8/tests/test_blocking_httpserver.py 2023-05-16 20:58:37.765109800 +0200 +++ new/pytest_httpserver-1.0.10/tests/test_blocking_httpserver.py 2024-02-17 17:55:07.352395800 +0100 @@ -40,7 +40,7 @@ assert server_connection.get(timeout=9).json() == response -@pytest.fixture +@pytest.fixture() def httpserver(): server = BlockingHTTPServer(timeout=1) server.start() @@ -97,7 +97,10 @@ ) with when_a_request_is_being_sent_to_the_server(request) as server_connection: - assert server_connection.get(timeout=9).text == "No handler found for this request" + assert ( + server_connection.get(timeout=9).text == "No handler found for request " + f"<Request 'http://localhost:{httpserver.port}/my/path' [GET]> with data b''." + ) def test_raises_assertion_error_when_request_was_not_responded(httpserver: BlockingHTTPServer): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_handler_errors.py new/pytest_httpserver-1.0.10/tests/test_handler_errors.py --- old/pytest_httpserver-1.0.8/tests/test_handler_errors.py 2021-11-09 10:15:44.000000000 +0100 +++ new/pytest_httpserver-1.0.10/tests/test_handler_errors.py 2024-02-17 17:55:07.352395800 +0100 @@ -7,7 +7,7 @@ def test_check_assertions_raises_handler_assertions(httpserver: HTTPServer): def handler(_): - assert 1 == 2 + assert False # noqa: PT015 httpserver.expect_request("/foobar").respond_with_handler(handler) @@ -29,7 +29,7 @@ httpserver.check_assertions() - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 httpserver.check_handler_errors() @@ -48,10 +48,10 @@ httpserver.check_assertions() - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 httpserver.check_handler_errors() - with pytest.raises(OSError): + with pytest.raises(OSError): # noqa: PT011 httpserver.check_handler_errors() httpserver.check_handler_errors() @@ -69,7 +69,7 @@ def test_check_raises_errors_in_order(httpserver): def handler1(_): - assert 1 == 2 + assert False # noqa: PT015 def handler2(_): pass # does nothing @@ -88,5 +88,5 @@ with pytest.raises(AssertionError): httpserver.check() - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 httpserver.check() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_json_matcher.py new/pytest_httpserver-1.0.10/tests/test_json_matcher.py --- old/pytest_httpserver-1.0.8/tests/test_json_matcher.py 2023-02-03 08:56:47.174314300 +0100 +++ new/pytest_httpserver-1.0.10/tests/test_json_matcher.py 2024-02-17 17:55:07.352395800 +0100 @@ -36,5 +36,5 @@ def test_data_and_json_mutually_exclusive(httpserver: HTTPServer): - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 httpserver.expect_request("/foo", json={}, data="foo") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_parse_qs.py new/pytest_httpserver-1.0.10/tests/test_parse_qs.py --- old/pytest_httpserver-1.0.8/tests/test_parse_qs.py 2023-05-16 20:58:37.765109800 +0200 +++ new/pytest_httpserver-1.0.10/tests/test_parse_qs.py 2024-02-17 17:55:07.352395800 +0100 @@ -1,10 +1,8 @@ +from __future__ import annotations + import urllib.parse -from typing import List -from typing import Tuple import pytest -import werkzeug.urls -from werkzeug.datastructures import MultiDict parse_qsl_semicolon_cases = [ ("&", []), @@ -19,12 +17,6 @@ ] -@pytest.mark.parametrize("qs,expected", parse_qsl_semicolon_cases) -def test_qsl(qs: str, expected: List[Tuple[bytes, bytes]]): +@pytest.mark.parametrize(("qs", "expected"), parse_qsl_semicolon_cases) +def test_qsl(qs: str, expected: list[tuple[bytes, bytes]]): assert urllib.parse.parse_qsl(qs, keep_blank_values=True) == expected - - -@pytest.mark.skip(reason="skipped to avoid werkzeug warnings") -@pytest.mark.parametrize("qs,expected", parse_qsl_semicolon_cases) -def test_qsl_werkzeug(qs: str, expected: List[Tuple[bytes, bytes]]): - assert werkzeug.urls.url_decode(qs) == MultiDict(expected) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_permanent.py new/pytest_httpserver-1.0.10/tests/test_permanent.py --- old/pytest_httpserver-1.0.8/tests/test_permanent.py 2023-05-16 20:58:37.765109800 +0200 +++ new/pytest_httpserver-1.0.10/tests/test_permanent.py 2024-02-17 17:55:07.352395800 +0100 @@ -106,3 +106,17 @@ response = requests.get(httpserver.url_for("/foobar")) assert response.json() == {"foo": "bar"} assert response.status_code == 200 + + +def test_request_handler_repr(httpserver: HTTPServer): + handler = httpserver.expect_request("/foo", method="POST") + assert ( + repr(handler) + == "<RequestHandler uri='/foo' method='POST' query_string=None headers={} data=None json=<UNDEFINED>>" + ) + + handler = httpserver.expect_request("/query", query_string={"a": "123"}) + assert ( + repr(handler) == "<RequestHandler uri='/query' method='__ALL' query_string={'a': '123'} " + "headers={} data=None json=<UNDEFINED>>" + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_port_changing.py new/pytest_httpserver-1.0.10/tests/test_port_changing.py --- old/pytest_httpserver-1.0.8/tests/test_port_changing.py 2023-02-03 08:56:47.174314300 +0100 +++ new/pytest_httpserver-1.0.10/tests/test_port_changing.py 2024-02-17 17:55:07.353395700 +0100 @@ -9,7 +9,7 @@ HOST_KEY = "PYTEST_HTTPSERVER_HOST" -@pytest.fixture +@pytest.fixture() def tmpenv(): old_vars = {} for key in (HOST_KEY, PORT_KEY): @@ -37,7 +37,7 @@ assert httpserver.port == int(os.environ[PORT_KEY]) -def test_get_httpserver_listen_address_with_env(tmpenv): +def test_get_httpserver_listen_address_with_env(tmpenv): # noqa: ARG001 address = get_httpserver_listen_address() assert address[0] == "5.5.5.5" assert address[1] == 12345 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_querymatcher.py new/pytest_httpserver-1.0.10/tests/test_querymatcher.py --- old/pytest_httpserver-1.0.8/tests/test_querymatcher.py 2023-01-18 23:01:30.866852500 +0100 +++ new/pytest_httpserver-1.0.10/tests/test_querymatcher.py 2024-02-17 17:55:07.353395700 +0100 @@ -28,7 +28,7 @@ def test_qm_boolean(): - qm = BooleanQueryMatcher(True) + qm = BooleanQueryMatcher(result=True) assert_match(qm, b"k1=v1") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.8/tests/test_release.py new/pytest_httpserver-1.0.10/tests/test_release.py --- old/pytest_httpserver-1.0.8/tests/test_release.py 2023-05-16 20:58:37.765109800 +0200 +++ new/pytest_httpserver-1.0.10/tests/test_release.py 2024-02-17 17:55:07.353395700 +0100 @@ -1,5 +1,4 @@ -# TODO: skip if poetry is not available or add mark to test it explicitly - +from __future__ import annotations import email import re @@ -10,15 +9,18 @@ from dataclasses import dataclass from pathlib import Path from typing import Iterable -from typing import Tuple import pytest import toml +# TODO: skip if poetry is not available or add mark to test it explicitly + + pytestmark = pytest.mark.release NAME = "pytest-httpserver" NAME_UNDERSCORE = NAME.replace("-", "_") +PY_MAX_VERSION = (3, 12) @pytest.fixture(scope="session") @@ -44,7 +46,7 @@ def extract(self): with zipfile.ZipFile(self.path) as zf: - zf.extractall(self.wheel_out_dir) + zf.extractall(self.wheel_out_dir) # noqa: S202 def get_meta(self, version: str, name: str = NAME_UNDERSCORE) -> email.message.Message: metadata_path = self.wheel_out_dir.joinpath(f"{name}-{version}.dist-info", "METADATA") @@ -64,7 +66,7 @@ def extract(self): with tarfile.open(self.path, mode="r:gz") as tf: - tf.extractall(self.sdist_out_dir) + tf.extractall(self.sdist_out_dir) # noqa: S202 @dataclass @@ -103,7 +105,7 @@ return pyproject["tool"]["poetry"]["version"] -def version_to_tuple(version: str) -> Tuple: +def version_to_tuple(version: str) -> tuple: return tuple([int(x) for x in version.split(".")]) @@ -120,20 +122,20 @@ pyproject_meta = pyproject["tool"]["poetry"] wheel_meta = build.wheel.get_meta(version=pyproject_meta["version"]) python_dependency = pyproject_meta["dependencies"]["python"] - m = re.match(r">=(\d+\.\d+),<(\d+\.\d+)", python_dependency) + m = re.match(r">=(\d+\.\d+)", python_dependency) if m: - min_version, max_version = m.groups() + min_version, *_ = m.groups() else: raise ValueError(python_dependency) min_version_tuple = version_to_tuple(min_version) - max_version_tuple = version_to_tuple(max_version) for classifier in wheel_meta.get_all("Classifier"): if classifier.startswith("Programming Language :: Python ::"): version_tuple = version_to_tuple(classifier.split("::")[-1].strip()) if len(version_tuple) > 1: - assert version_tuple >= min_version_tuple and version_tuple < max_version_tuple + assert version_tuple >= min_version_tuple + assert version_tuple <= PY_MAX_VERSION def test_wheel_no_extra_contents(build: Build, version: str): @@ -168,7 +170,6 @@ "pyproject.toml", "pytest_httpserver", "README.md", - "setup.py", "tests", }, "doc": {