Skip to content

Commit 7e39e4d

Browse files
committed
introduce @field(disable=True)
1 parent fad17a4 commit 7e39e4d

File tree

2 files changed

+148
-10
lines changed

2 files changed

+148
-10
lines changed

tests/test_fields.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import random
3+
from typing import Optional
34

45
import attrs
56
import pytest
@@ -26,7 +27,7 @@
2627
item_from_fields,
2728
item_from_fields_sync,
2829
)
29-
from web_poet.fields import get_fields_dict
30+
from web_poet.fields import FieldInfo, get_fields_dict
3031

3132

3233
@attrs.define
@@ -522,3 +523,110 @@ def b(self):
522523
return None
523524

524525
assert set(get_fields_dict(B)) == {"a", "b", "mixin"}
526+
527+
528+
@pytest.mark.asyncio
529+
async def test_field_disabled() -> None:
530+
@attrs.define
531+
class Item:
532+
x: int
533+
y: Optional[int] = None
534+
z: Optional[int] = None
535+
536+
class Page(ItemPage[Item]):
537+
@field
538+
def x(self) -> int:
539+
return 1
540+
541+
@field(disabled=False)
542+
def y(self) -> int:
543+
return 2
544+
545+
@field(disabled=True)
546+
def z(self) -> int:
547+
return 3
548+
549+
page = Page()
550+
assert await page.to_item() == Item(x=1, y=2)
551+
assert page.x == 1
552+
assert page.y == 2
553+
assert page.z == 3
554+
555+
fields_dict_instance = get_fields_dict(page)
556+
fields_dict_class = get_fields_dict(Page)
557+
558+
for info in [fields_dict_class, fields_dict_instance]:
559+
assert info["x"] == FieldInfo(name="x", meta=None, out=None, disabled=False)
560+
assert info["y"] == FieldInfo(name="y", meta=None, out=None, disabled=False)
561+
562+
fields_dict_instance = get_fields_dict(page, include_disabled=True)
563+
fields_dict_class = get_fields_dict(Page, include_disabled=True)
564+
565+
for info in [fields_dict_class, fields_dict_instance]:
566+
assert info["x"] == FieldInfo(name="x", meta=None, out=None, disabled=False)
567+
assert info["y"] == FieldInfo(name="y", meta=None, out=None, disabled=False)
568+
assert info["z"] == FieldInfo(name="z", meta=None, out=None, disabled=True)
569+
570+
# The subclass should properly reflect any changes to the ``disable`` value
571+
572+
class SubPage(Page):
573+
"""Flicks the switch for ``y`` and ``z``."""
574+
575+
@field(disabled=True)
576+
def y(self) -> int:
577+
return 2
578+
579+
@field(disabled=False)
580+
def z(self) -> int:
581+
return 3
582+
583+
subpage = SubPage()
584+
assert await subpage.to_item() == Item(x=1, z=3)
585+
assert subpage.x == 1
586+
assert subpage.y == 2
587+
assert subpage.z == 3
588+
589+
fields_dict_instance = get_fields_dict(subpage)
590+
fields_dict_class = get_fields_dict(SubPage)
591+
592+
for info in [fields_dict_class, fields_dict_instance]:
593+
assert info["x"] == FieldInfo(name="x", meta=None, out=None, disabled=False)
594+
assert info["z"] == FieldInfo(name="z", meta=None, out=None, disabled=False)
595+
596+
fields_dict_instance = get_fields_dict(subpage, include_disabled=True)
597+
fields_dict_class = get_fields_dict(SubPage, include_disabled=True)
598+
599+
for info in [fields_dict_class, fields_dict_instance]:
600+
assert info["x"] == FieldInfo(name="x", meta=None, out=None, disabled=False)
601+
assert info["y"] == FieldInfo(name="y", meta=None, out=None, disabled=True)
602+
assert info["z"] == FieldInfo(name="z", meta=None, out=None, disabled=False)
603+
604+
# Disabling fields that are required in the item cls would error out.
605+
606+
class BadSubPage(Page):
607+
@field(disabled=True)
608+
def x(self) -> int:
609+
return 1
610+
611+
badsubpage = BadSubPage()
612+
613+
with pytest.raises(TypeError):
614+
await badsubpage.to_item()
615+
616+
assert badsubpage.x == 1
617+
assert badsubpage.y == 2
618+
assert badsubpage.z == 3
619+
620+
fields_dict_instance = get_fields_dict(badsubpage)
621+
fields_dict_class = get_fields_dict(BadSubPage)
622+
623+
for info in [fields_dict_class, fields_dict_instance]:
624+
assert info["y"] == FieldInfo(name="y", meta=None, out=None, disabled=False)
625+
626+
fields_dict_instance = get_fields_dict(badsubpage, include_disabled=True)
627+
fields_dict_class = get_fields_dict(BadSubPage, include_disabled=True)
628+
629+
for info in [fields_dict_class, fields_dict_instance]:
630+
assert info["x"] == FieldInfo(name="x", meta=None, out=None, disabled=True)
631+
assert info["y"] == FieldInfo(name="y", meta=None, out=None, disabled=False)
632+
assert info["z"] == FieldInfo(name="z", meta=None, out=None, disabled=True)

web_poet/fields.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
_FIELDS_INFO_ATTRIBUTE_WRITE = "_web_poet_fields_info_temp"
1717

1818

19+
def _fields_template():
20+
return {"enabled": {}, "disabled": {}}
21+
22+
1923
@attrs.define
2024
class FieldInfo:
2125
"""Information about a field"""
@@ -29,6 +33,9 @@ class FieldInfo:
2933
#: field processors
3034
out: Optional[List[Callable]] = None
3135

36+
#: when set to ``True``, the field is not populated on ``.to_item()`` calls.
37+
disabled: bool = False
38+
3239

3340
class FieldsMixin:
3441
"""A mixin which is required for a class to support fields"""
@@ -39,11 +46,24 @@ def __init_subclass__(cls, **kwargs):
3946
# between subclasses, i.e. a decorator in a subclass doesn't affect
4047
# the base class. This is done by making decorator write to a
4148
# temporary location, and then merging it all on subclass creation.
42-
this_class_fields = getattr(cls, _FIELDS_INFO_ATTRIBUTE_WRITE, {})
43-
base_class_fields = getattr(cls, _FIELDS_INFO_ATTRIBUTE_READ, {})
49+
this_class_fields = getattr(
50+
cls, _FIELDS_INFO_ATTRIBUTE_WRITE, _fields_template()
51+
)
52+
base_class_fields = getattr(
53+
cls, _FIELDS_INFO_ATTRIBUTE_READ, _fields_template()
54+
)
4455
if base_class_fields or this_class_fields:
45-
fields = {**base_class_fields, **this_class_fields}
46-
setattr(cls, _FIELDS_INFO_ATTRIBUTE_READ, fields)
56+
enabled = {**base_class_fields["enabled"], **this_class_fields["enabled"]}
57+
disabled = {**this_class_fields["disabled"]}
58+
for name, info in this_class_fields["disabled"].items():
59+
if name in enabled:
60+
del enabled[name]
61+
disabled[name] = info
62+
setattr(
63+
cls,
64+
_FIELDS_INFO_ATTRIBUTE_READ,
65+
{"enabled": enabled, "disabled": disabled},
66+
)
4767
with suppress(AttributeError):
4868
delattr(cls, _FIELDS_INFO_ATTRIBUTE_WRITE)
4969

@@ -54,6 +74,7 @@ def field(
5474
cached: bool = False,
5575
meta: Optional[dict] = None,
5676
out: Optional[List[Callable]] = None,
77+
disabled: bool = False,
5778
):
5879
"""
5980
Page Object method decorated with ``@field`` decorator becomes a property,
@@ -85,10 +106,11 @@ def __init__(self, method):
85106

86107
def __set_name__(self, owner, name):
87108
if not hasattr(owner, _FIELDS_INFO_ATTRIBUTE_WRITE):
88-
setattr(owner, _FIELDS_INFO_ATTRIBUTE_WRITE, {})
109+
setattr(owner, _FIELDS_INFO_ATTRIBUTE_WRITE, _fields_template())
89110

90-
field_info = FieldInfo(name=name, meta=meta, out=out)
91-
getattr(owner, _FIELDS_INFO_ATTRIBUTE_WRITE)[name] = field_info
111+
field_info = FieldInfo(name=name, meta=meta, out=out, disabled=disabled)
112+
switch = "disabled" if disabled else "enabled"
113+
getattr(owner, _FIELDS_INFO_ATTRIBUTE_WRITE)[switch][name] = field_info
92114

93115
def __get__(self, instance, owner=None):
94116
return self.unbound_method(instance)
@@ -125,12 +147,20 @@ def processed(*args, **kwargs):
125147
return _field
126148

127149

128-
def get_fields_dict(cls_or_instance) -> Dict[str, FieldInfo]:
150+
def get_fields_dict(
151+
cls_or_instance, include_disabled: bool = False
152+
) -> Dict[str, FieldInfo]:
129153
"""Return a dictionary with information about the fields defined
130154
for the class: keys are field names, and values are
131155
:class:`web_poet.fields.FieldInfo` instances.
132156
"""
133-
return getattr(cls_or_instance, _FIELDS_INFO_ATTRIBUTE_READ, {})
157+
fields_info = getattr(
158+
cls_or_instance, _FIELDS_INFO_ATTRIBUTE_READ, _fields_template()
159+
)
160+
fields_dict = fields_info["enabled"]
161+
if include_disabled:
162+
fields_dict.update(fields_info["disabled"])
163+
return fields_dict
134164

135165

136166
T = TypeVar("T")

0 commit comments

Comments
 (0)