Skip to content

Commit

Permalink
refactor: Simplify Python code samples for failure reproduction
Browse files Browse the repository at this point in the history
  • Loading branch information
Stranger6667 committed Oct 16, 2023
1 parent bfe0522 commit 316d80c
Show file tree
Hide file tree
Showing 19 changed files with 210 additions and 133 deletions.
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Changelog
- Refined generated reproduction code and shortened ``X-Schemathesis-TestCaseId`` for easier debugging. `#1801`_
- Add ``case`` as the first argument to ``AuthContext.set``. Previous calling convention is still supported. `#1788`_
- Disable the 'explain' phase in Hypothesis to improve performance. `#1808`_
- Simplify Python code samples for failure reproduction.
- Do not display ``InsecureRequestWarning`` in CLI output if the user explicitly provided ``--request-tls-verify=false``. `#1780`_
- Enhance CLI output for schema loading and internal errors, providing clearer diagnostics and guidance. `#1781`_, `#1517`_, `#1472`_

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ dependencies = [
"backoff>=2.1.2,<3.0",
"click>=7.0,<9.0",
"colorama>=0.4,<1.0",
"curlify>=2.2.1,<3.0",
"httpx>=0.22.0,<1.0",
"hypothesis>=6.31.6,<7",
"hypothesis_graphql>=0.9.0,<1",
Expand Down
2 changes: 1 addition & 1 deletion src/schemathesis/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .. import fixups as _fixups
from .. import runner, service
from .. import targets as targets_module
from ..code_samples import CodeSampleStyle
from ..constants import (
API_NAME_ENV_VAR,
BASE_URL_ENV_VAR,
Expand All @@ -32,7 +33,6 @@
HOOKS_MODULE_ENV_VAR,
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
WAIT_FOR_SCHEMA_ENV_VAR,
CodeSampleStyle,
DataGenerationMethod,
)
from ..exceptions import SchemaError
Expand Down
3 changes: 2 additions & 1 deletion src/schemathesis/cli/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from requests import PreparedRequest, RequestException

from .. import exceptions, experimental, throttling, utils
from ..constants import CodeSampleStyle, DataGenerationMethod
from ..code_samples import CodeSampleStyle
from ..constants import DataGenerationMethod
from ..service.hosts import get_temporary_hosts_file
from ..stateful import Stateful
from ..types import PathLike
Expand Down
2 changes: 1 addition & 1 deletion src/schemathesis/cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import hypothesis

from ..constants import CodeSampleStyle
from ..code_samples import CodeSampleStyle
from ..runner.serialization import SerializedTestResult


Expand Down
2 changes: 1 addition & 1 deletion src/schemathesis/cli/output/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@

from ... import service
from ..._compat import metadata
from ...code_samples import CodeSampleStyle
from ...constants import (
DISCORD_LINK,
FLAKY_FAILURE_MESSAGE,
REPORT_SUGGESTION_ENV_VAR,
SCHEMATHESIS_TEST_CASE_HEADER,
CodeSampleStyle,
__version__,
)
from ...experimental import GLOBAL_EXPERIMENTS
Expand Down
131 changes: 131 additions & 0 deletions src/schemathesis/code_samples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from enum import Enum
from shlex import quote
from typing import Any, Optional

from requests.structures import CaseInsensitiveDict
from requests.utils import default_headers

from .constants import SCHEMATHESIS_TEST_CASE_HEADER, DataGenerationMethod

DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
# These headers are added automatically by Schemathesis or `requests`.
# Do not show them in code samples to make them more readable
EXCLUDED_HEADERS = CaseInsensitiveDict(
{
"Content-Length": None,
"Transfer-Encoding": None,
SCHEMATHESIS_TEST_CASE_HEADER: None,
**default_headers(),
}
)


class CodeSampleStyle(str, Enum):
"""Controls the style of code samples for failure reproduction."""

python = "python"
curl = "curl"

@classmethod
def default(cls) -> "CodeSampleStyle":
return cls.curl

@classmethod
def from_str(cls, value: str) -> "CodeSampleStyle":
try:
return cls[value]
except KeyError:
available_styles = ", ".join(cls)
raise ValueError(
f"Invalid value for code sample style: {value}. Available styles: {available_styles}"
) from None

def generate(
self,
*,
method: str,
url: str,
body: Any,
headers: CaseInsensitiveDict,
verify: bool,
include_headers: Optional[CaseInsensitiveDict] = None,
) -> str:
"""Generate a code snippet for making HTTP requests."""
handlers = {
self.curl: _generate_curl,
self.python: _generate_requests,
}
return handlers[self](
method=method, url=url, body=body, headers=_filter_headers(headers, include_headers), verify=verify
)


def _filter_headers(
headers: CaseInsensitiveDict, include_headers: Optional[CaseInsensitiveDict] = None
) -> CaseInsensitiveDict:
include_headers = include_headers or CaseInsensitiveDict({})
return CaseInsensitiveDict(
{key: val for key, val in headers.items() if key not in EXCLUDED_HEADERS or key in include_headers}
)


def _generate_curl(
*,
method: str,
url: str,
body: Any,
headers: CaseInsensitiveDict,
verify: bool,
) -> str:
command = f"curl -X {method}"
for key, value in headers.items():
header = f"{key}: {value}"
command += f" -H {quote(header)}"
if body:
if isinstance(body, bytes):
body = body.decode("utf-8", errors="replace")
command += f" -d {quote(body)}"
if not verify:
command += " --insecure"
return f"{command} {quote(url)}"


def _generate_requests(
*,
method: str,
url: str,
body: Any,
headers: CaseInsensitiveDict,
verify: bool,
) -> str:
url = _escape_single_quotes(url)
command = f"requests.{method.lower()}('{url}'"
if body:
command += f", data={repr(body)}"
if headers:
command += f", headers={repr(headers)}"
if not verify:
command += ", verify=False"
command += ")"
return command


def _escape_single_quotes(url: str) -> str:
"""Escape single quotes in a string, so it is usable as in generated Python code.
The usual ``str.replace`` is not suitable as it may convert already escaped quotes to not-escaped.
"""
result = []
escape = False
for char in url:
if escape:
result.append(char)
escape = False
elif char == "\\":
result.append(char)
escape = True
elif char == "'":
result.append("\\'")
else:
result.append(char)
return "".join(result)
41 changes: 0 additions & 41 deletions src/schemathesis/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

import pytest
from packaging import version
from requests.structures import CaseInsensitiveDict
from requests.utils import default_headers

from ._compat import metadata

Expand Down Expand Up @@ -74,42 +72,3 @@ def is_negative(self) -> bool:


DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
# These headers are added automatically by Schemathesis or `requests`.
# Do not show them in code samples to make them more readable
CURL_EXCLUDED_HEADERS = CaseInsensitiveDict(
{
"Content-Length": None,
"Transfer-Encoding": None,
SCHEMATHESIS_TEST_CASE_HEADER: None,
**default_headers(),
}
)
REQUESTS_EXCLUDED_HEADERS = CaseInsensitiveDict({"Content-Type": None, **CURL_EXCLUDED_HEADERS})


class CodeSampleStyle(str, Enum):
"""Controls the style of code samples for failure reproduction."""

python = "python"
curl = "curl"

@classmethod
def default(cls) -> "CodeSampleStyle":
return cls.curl

@classmethod
def from_str(cls, value: str) -> "CodeSampleStyle":
try:
return cls[value]
except KeyError:
available_styles = ", ".join(cls)
raise ValueError(
f"Invalid value for code sample style: {value}. Available styles: {available_styles}"
) from None

@property
def ignored_headers(self) -> CaseInsensitiveDict:
return {
self.python: REQUESTS_EXCLUDED_HEADERS,
self.curl: CURL_EXCLUDED_HEADERS,
}[self]
3 changes: 2 additions & 1 deletion src/schemathesis/lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

from ._compat import MultipleFailures
from .auths import AuthStorage
from .constants import FLAKY_FAILURE_MESSAGE, CodeSampleStyle
from .code_samples import CodeSampleStyle
from .constants import FLAKY_FAILURE_MESSAGE
from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
from .hooks import HookDispatcher, HookScope
from .models import APIOperation
Expand Down
Loading

0 comments on commit 316d80c

Please sign in to comment.