Skip to content

Commit

Permalink
Mapping protocol for schemas & direct access to endpoint strategies. C…
Browse files Browse the repository at this point in the history
…loses #98
  • Loading branch information
Stranger6667 committed Oct 4, 2019
1 parent f8f23d3 commit c3482a5
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 4 deletions.
28 changes: 28 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,34 @@ With this Swagger schema example, there will be a case with body ``{"name": "Dog

NOTE. Schemathesis supports only examples in ``parameters`` at the moment, examples of individual properties are not supported.

Direct strategies access
~~~~~~~~~~~~~~~~~~~~~~~~

For convenience you can explore the schemas and strategies manually:

.. code:: python
>>> import schemathesis
>>> schema = schemathesis.from_uri("http://0.0.0.0:8080/petstore.json")
>>> endpoint = schema["/v2/pet"]["POST"]
>>> strategy = endpoint.as_strategy()
>>> strategy.example()
Case(
path='/v2/pet',
method='POST',
path_parameters={},
headers={},
cookies={},
query={},
body={
'name': '\x15.\x13\U0008f42a',
'photoUrls': ['\x08\U0009f29a', '\U000abfd6\U000427c4', '']
},
form_data={}
)
Schema instances implement `Mapping` protocol.

Lazy loading
~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Added

- CLI tool invoked by the ``schemathesis`` command. `#30`_
- New arguments ``api_options``, ``loader_options`` and ``loader`` for test executor. `#90`_
- A mapping interface for schemas & convenience methods for direct strategies access. `#98`_

Fixed
~~~~~
Expand Down Expand Up @@ -178,6 +179,7 @@ Fixed
.. _0.2.0: https://github.com/kiwicom/schemathesis/compare/v0.1.0...v0.2.0

.. _#99: https://github.com/kiwicom/schemathesis/issues/99
.. _#98: https://github.com/kiwicom/schemathesis/issues/98
.. _#90: https://github.com/kiwicom/schemathesis/issues/90
.. _#78: https://github.com/kiwicom/schemathesis/issues/78
.. _#75: https://github.com/kiwicom/schemathesis/issues/75
Expand Down
2 changes: 1 addition & 1 deletion src/schemathesis/_hypothesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

def create_test(endpoint: Endpoint, test: Callable, settings: Optional[hypothesis.settings] = None) -> Callable:
"""Create a Hypothesis test."""
strategy = get_case_strategy(endpoint)
strategy = endpoint.as_strategy()
wrapped_test = hypothesis.given(case=strategy)(test)
if settings is not None:
wrapped_test = settings(wrapped_test)
Expand Down
6 changes: 6 additions & 0 deletions src/schemathesis/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Dict

import attr
from hypothesis.searchstrategy import SearchStrategy

from .types import Body, Cookies, FormData, Headers, PathParameters, Query

Expand Down Expand Up @@ -41,3 +42,8 @@ class Endpoint:
query: Query = attr.ib(factory=empty_object)
body: Body = attr.ib(factory=empty_object)
form_data: FormData = attr.ib(factory=empty_object)

def as_strategy(self) -> SearchStrategy:
from ._hypothesis import get_case_strategy

return get_case_strategy(self)
37 changes: 34 additions & 3 deletions src/schemathesis/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
They give only static definitions of endpoints.
"""
import itertools
from collections.abc import Mapping
from copy import deepcopy
from typing import Any, Callable, Dict, Generator, Iterator, List, Optional, Tuple, Union, overload
from urllib.parse import urljoin
Expand All @@ -22,12 +23,29 @@
from .utils import NOT_SET


@attr.s(slots=True)
class BaseSchema:
@attr.s()
class BaseSchema(Mapping):
raw_schema: Dict[str, Any] = attr.ib()
method: Optional[Filter] = attr.ib(default=None)
endpoint: Optional[Filter] = attr.ib(default=None)

def __iter__(self) -> Iterator:
return iter(self.endpoints)

def __getitem__(self, item: str) -> Dict[str, Endpoint]:
return self.endpoints[item]

def __len__(self) -> int:
return len(self.endpoints)

@property
def endpoints(self) -> Dict[str, Dict[str, Endpoint]]:
if not hasattr(self, "_endpoints"):
# pylint: disable=attribute-defined-outside-init
endpoints = self.get_all_endpoints()
self._endpoints = endpoints_to_dict(endpoints)
return self._endpoints

@property
def resolver(self) -> jsonschema.RefResolver:
if not hasattr(self, "_resolver"):
Expand Down Expand Up @@ -60,6 +78,10 @@ def wrapper(func: Callable) -> Callable:


class SwaggerV20(BaseSchema):
def __repr__(self) -> str:
info = self.raw_schema["info"]
return f"{self.__class__.__name__} for {info['title']} ({info['version']})"

@property
def base_path(self) -> str:
"""Base path for the schema."""
Expand Down Expand Up @@ -188,7 +210,7 @@ def prepare(item: Dict[str, Any]) -> Dict[str, Any]:
return item


class OpenApi30(SwaggerV20):
class OpenApi30(SwaggerV20): # pylint: disable=too-many-ancestors
def make_endpoint(
self, full_path: str, method: str, parameters: Iterator[Dict[str, Any]], definition: Dict[str, Any]
) -> Endpoint:
Expand Down Expand Up @@ -228,3 +250,12 @@ def get_common_parameters(methods: Dict[str, Any]) -> List[Dict[str, Any]]:
if common_parameters is not None:
return deepcopy(common_parameters)
return []


def endpoints_to_dict(endpoints: Generator[Endpoint, None, None]) -> Dict[str, Dict[str, Endpoint]]:
output: Dict[str, Dict[str, Endpoint]] = {}
for endpoint in endpoints:
output.setdefault(endpoint.path, {})
# case insensitive dict?
output[endpoint.path][endpoint.method] = endpoint
return output
39 changes: 39 additions & 0 deletions test/test_direct_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import hypothesis.strategies as st

from schemathesis.models import Case, Endpoint


def test_contains(swagger_20):
assert "/v1/users" in swagger_20


def test_getitem(swagger_20):
assert "_endpoints" not in swagger_20.__dict__
assert isinstance(swagger_20["/v1/users"], dict)
# Check cached access
assert "_endpoints" in swagger_20.__dict__
assert isinstance(swagger_20["/v1/users"], dict)


def test_len(swagger_20):
assert len(swagger_20) == 1


def test_iter(swagger_20):
assert list(swagger_20) == ["/v1/users"]


def test_repr(swagger_20):
assert str(swagger_20) == "SwaggerV20 for Sample API (1.0.0)"


def test_endpoint_access(swagger_20):
assert isinstance(swagger_20["/v1/users"]["GET"], Endpoint)


def test_as_strategy(swagger_20):
strategy = swagger_20["/v1/users"]["GET"].as_strategy()
assert isinstance(strategy, st.SearchStrategy)
assert strategy.example() == Case(
path="/v1/users", method="GET", path_parameters={}, headers={}, cookies={}, query={}, body={}, form_data={}
)

0 comments on commit c3482a5

Please sign in to comment.