|
1 | | -from typing import Optional, Dict, Any, ByteString, Union |
| 1 | +import inspect |
| 2 | +from typing import Optional, Dict, Any, ByteString, Union, Set |
2 | 3 | from contextlib import suppress |
3 | 4 |
|
4 | 5 | import attr |
@@ -32,35 +33,65 @@ class ResponseData: |
32 | 33 | headers: Optional[Dict[Union[str, ByteString], Any]] = None |
33 | 34 |
|
34 | 35 |
|
35 | | -class Meta: |
36 | | - """Container class that could contain any arbitrary data. |
| 36 | +class Meta(dict): |
| 37 | + """Container class that could contain any arbitrary data to be passed into |
| 38 | + a Page Object. |
37 | 39 |
|
38 | | - Using this is more useful to pass things around compared to a ``dict`` due |
39 | | - to these following characteristics: |
| 40 | + This is basically a subclass of a ``dict`` but adds some additional |
| 41 | + functionalities to ensure consistent and compatible Page Objects across |
| 42 | + different use cases: |
40 | 43 |
|
41 | | - - You can use Python's "." attribute syntax for it. |
42 | | - - Accessing attributes that are not existing won't result in errors. |
43 | | - Instead, a ``None`` value will be returned. |
44 | | - - The same goes for deleting attributes that don't exist wherein errors |
45 | | - will be suppressed. |
| 44 | + * A class variable named ``required_data`` to ensure consistent |
| 45 | + arguments. If it's instantiated with missing ``keys`` from |
| 46 | + ``required_data``, then a ``ValueError`` is raised. |
46 | 47 |
|
47 | | - This makes the code simpler by avoiding try/catch, checking an attribute's |
48 | | - existence, using ``get()``, etc. |
| 48 | + * Ensures that some params with data types that are difficult to |
| 49 | + provide or pass like ``lambdas`` are checked. Otherwise, a ``ValueError`` |
| 50 | + is raised. |
49 | 51 | """ |
50 | 52 |
|
51 | | - def __init__(self, **kwargs): |
52 | | - object.__setattr__(self, "_data", kwargs) |
53 | | - |
54 | | - def __getattr__(self, key): |
55 | | - return self._data.get(key) |
56 | | - |
57 | | - def __delattr__(self, key): |
58 | | - with suppress(KeyError): |
59 | | - del self._data[key] |
60 | | - |
61 | | - def __setattr__(self, key, value): |
62 | | - self._data[key] = value |
63 | | - |
64 | | - def __repr__(self): |
65 | | - contents = ", ".join([f"{k}={v!r}" for k, v in self._data.items()]) |
66 | | - return f"Meta({contents})" |
| 53 | + # Contains the required "keys" when instantiating and setting attributes. |
| 54 | + required_data: Set = set() |
| 55 | + |
| 56 | + # Any "value" that returns True for the functions here are not allowed. |
| 57 | + restrictions: Dict = { |
| 58 | + inspect.ismodule: "module", |
| 59 | + inspect.isclass: "class", |
| 60 | + inspect.ismethod: "method", |
| 61 | + inspect.isfunction: "function", |
| 62 | + inspect.isgenerator: "generator", |
| 63 | + inspect.isgeneratorfunction: "generator", |
| 64 | + inspect.iscoroutine: "coroutine", |
| 65 | + inspect.isawaitable: "awaitable", |
| 66 | + inspect.istraceback: "traceback", |
| 67 | + inspect.isframe: "frame", |
| 68 | + } |
| 69 | + |
| 70 | + def __init__(self, *args, **kwargs) -> None: |
| 71 | + missing_required_keys = self.required_data - kwargs.keys() |
| 72 | + if missing_required_keys: |
| 73 | + raise ValueError( |
| 74 | + f"These keys are required for instantiation: {missing_required_keys}" |
| 75 | + ) |
| 76 | + for val in kwargs.values(): |
| 77 | + self.is_restricted_value(val) |
| 78 | + super().__init__(*args, **kwargs) |
| 79 | + |
| 80 | + def __setitem__(self, key: Any, value: Any) -> None: |
| 81 | + self.is_restricted_value(value) |
| 82 | + super().__setattr__(key, value) |
| 83 | + |
| 84 | + def is_restricted_value(self, value: Any) -> None: |
| 85 | + """Raises an error if a given value isn't allowed inside the meta. |
| 86 | +
|
| 87 | + This behavior can be controlled by tweaking the class variable |
| 88 | + :meth:`~.web_poet.page_inputs.Meta.restrictions`. |
| 89 | + """ |
| 90 | + violations = [] |
| 91 | + |
| 92 | + for restrictor, err in self.restrictions.items(): |
| 93 | + if restrictor(value): |
| 94 | + violations.append(f"{err} is not allowed: {value}") |
| 95 | + |
| 96 | + if violations: |
| 97 | + raise ValueError(f"Found these issues: {', '.join(violations)}") |
0 commit comments