|
| 1 | +fromtypingimportAny |
| 2 | + |
| 3 | +importpytest |
| 4 | + |
| 5 | +fromscrapy.http.requestimportNO_CALLBACK,Request |
| 6 | +fromscrapy.http.request.formimportFormRequest |
| 7 | +fromscrapy.http.request.json_requestimportJsonRequest |
| 8 | + |
| 9 | +_attr_value_map:dict[str,Any]= { |
| 10 | +"url":"http://example.com/test", |
| 11 | +"callback":NO_CALLBACK, |
| 12 | +"method":"POST", |
| 13 | +"headers": { |
| 14 | +b"X-Test-Header": [b"1"], |
| 15 | +# `JsonRequest` will eventually add those even if they are not present |
| 16 | +b"Accept": [b"application/json, text/javascript, */*; q=0.01"], |
| 17 | +b"Content-Type": [b"application/json"], |
| 18 | + }, |
| 19 | +"body":b"hello", |
| 20 | +"cookies": {"a":"1"}, |
| 21 | +"meta": {"k":"v"}, |
| 22 | +"encoding":"koi8-r", |
| 23 | +"priority":5, |
| 24 | +"dont_filter":True, |
| 25 | +"errback":NO_CALLBACK, |
| 26 | +"flags": ["f1","f2"], |
| 27 | +"cb_kwargs": {"x":1}, |
| 28 | +} |
| 29 | + |
| 30 | + |
| 31 | +def_assert_equal_attribute(obj:Request,attr:str,expected:Any): |
| 32 | +val=getattr(obj,attr) |
| 33 | +ifattr=="headers": |
| 34 | +# Headers object -> dict |
| 35 | +assertdict(val)==dict(expected) |
| 36 | +elifattr=="body": |
| 37 | +# body should be bytes |
| 38 | +assertval==expected |
| 39 | +else: |
| 40 | +assertval==expected |
| 41 | + |
| 42 | + |
| 43 | +@pytest.mark.parametrize("request_class", [Request,JsonRequest,FormRequest]) |
| 44 | +@pytest.mark.parametrize("attr",Request.attributes) |
| 45 | +deftest_attribute_setattr_and_replace_behavior( |
| 46 | +request_class:type[Request],attr:str |
| 47 | +): |
| 48 | +"""Ensure current assignment and replace semantics for Request.attributes. |
| 49 | +
|
| 50 | + - If setattr(obj, attr, val) works today, it must keep working and |
| 51 | + replace() should carry the value over. |
| 52 | + - If setattr(obj, attr, val) raises AttributeError today (read-only), |
| 53 | + replace(**{attr: val}) should still allow creating a Request with that attr. |
| 54 | + """ |
| 55 | +r=request_class("http://example.com/") |
| 56 | + |
| 57 | +ifattrnotin_attr_value_map: |
| 58 | +return |
| 59 | + |
| 60 | +val=_attr_value_map[attr] |
| 61 | + |
| 62 | +# first try direct setattr |
| 63 | +try: |
| 64 | +setattr(r,attr,val) |
| 65 | +exceptAttributeError: |
| 66 | +# attribute is read-only |
| 67 | +# ensure replace(**{attr: val}) creates a new request with that value |
| 68 | +r2=r.replace(**{attr:val}) |
| 69 | +_assert_equal_attribute(r2,attr,val) |
| 70 | +# original request must remain unchanged (unless replace mutated it) |
| 71 | +# (for read-only attributes we expect original not to equal val) |
| 72 | +ifgetattr(r,attr)==val: |
| 73 | +pytest.fail( |
| 74 | +f"Attribute{attr} unexpectedly mutated original Request when " |
| 75 | +f"it should have been read-only (direct setattr raised)." |
| 76 | + ) |
| 77 | +else: |
| 78 | +# direct setattr succeeded; attribute must reflect assigned value |
| 79 | +_assert_equal_attribute(r,attr,val) |
| 80 | + |
| 81 | +# and replace() must preserve it (replace uses getattr(self, x)) |
| 82 | +r2=r.replace() |
| 83 | +_assert_equal_attribute(r2,attr,getattr(r,attr)) |