Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meta Validation #32

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/advanced/meta.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,26 @@ From the example above, we can see how :class:`~.Meta` is able to arbitrarily
limit the pagination behavior by passing an optional **max_pages** info. Take
note that a ``default_max_pages`` value is also present in the Page Object in
case the :class:`~.Meta` instance did not provide it.

Value Restrictions
------------------

From the examples above, you may notice that we can access :class:`~.Meta` with
a ``dict`` interface since it's simply a subclass of it. However, :class:`~.Meta`
posses some extendable features on top of being a ``dict``.

Specifically, :class:`~.Meta` is able to restrict any value passed based on its
type. For example, if any of these values are passed, then a ``ValueError`` is
raised:

* module
* class
* method or function
* generator
* coroutine or awaitable
* traceback
* frame

This is to ensure that frameworks using **web-poet** are able safely use values
passed into :class:`~.Meta` as they could be passed via CLI, web forms, HTTP API
calls, etc.
21 changes: 20 additions & 1 deletion tests/test_page_inputs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from web_poet.page_inputs import ResponseData
import pytest
import asyncio

from web_poet.page_inputs import ResponseData, Meta


def test_html_response():
Expand All @@ -11,3 +14,19 @@ def test_html_response():
response = ResponseData("url", "content", 200, {"User-Agent": "test agent"})
assert response.status == 200
assert response.headers["User-Agent"] == "test agent"


def test_meta_restriction():
# Any value that conforms with `Meta.restrictions` raises an error
with pytest.raises(ValueError) as err:
Meta(func=lambda x: x + 1)

with pytest.raises(ValueError) as err:
Meta(class_=ResponseData)

# These are allowed though
m = Meta(x="hi", y=2.2, z={"k": "v"})
m["allowed"] = [1, 2, 3]

with pytest.raises(ValueError) as err:
m["not_allowed"] = asyncio.sleep(1)
50 changes: 48 additions & 2 deletions web_poet/page_inputs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Optional, Dict, Any, ByteString, Union
import inspect
from typing import Optional, Dict, Any, ByteString, Union, Set
from contextlib import suppress

import attr

Expand Down Expand Up @@ -34,6 +36,50 @@ class ResponseData:
class Meta(dict):
"""Container class that could contain any arbitrary data to be passed into
a Page Object.

This is basically a subclass of a ``dict`` that adds the ability to check
if any of the assigned values are not allowed. This ensures that some input
parameters with data types that are difficult to provide or pass via CLI
like ``lambdas`` are checked. Otherwise, a ``ValueError`` is raised.
"""

pass
# Any "value" that returns True for the functions here are not allowed.
restrictions: Dict = {
inspect.ismodule: "module",
inspect.isclass: "class",
inspect.ismethod: "method",
inspect.isfunction: "function",
inspect.isgenerator: "generator",
inspect.isgeneratorfunction: "generator",
inspect.iscoroutine: "coroutine",
inspect.isawaitable: "awaitable",
inspect.istraceback: "traceback",
inspect.isframe: "frame",
}

def __init__(self, *args, **kwargs) -> None:
for val in kwargs.values():
self.enforce_value_restriction(val)
super().__init__(*args, **kwargs)

def __setitem__(self, key: Any, value: Any) -> None:
self.enforce_value_restriction(value)
super().__setattr__(key, value)

def enforce_value_restriction(self, value: Any) -> None:
"""Raises a ``ValueError`` if a given value isn't allowed inside the meta.

This method is called during :class:`~.Meta` instantiation and setting
new values in an existing instance.

This behavior can be controlled by tweaking the class variable named
``restrictions``.
"""
violations = []

for restrictor, err in self.restrictions.items():
if restrictor(value):
violations.append(f"{err} is not allowed: {value}")

if violations:
raise ValueError(f"Found these issues: {', '.join(violations)}")