Skip to content

Commit 672c58b

Browse files
authored
refactor!: mark secondary arguments as keyword-only (#2318)
Secondary arguments across the public API are now keyword-only, the same way as in apify/apify-client-python#766. The main argument of each call (key, value, data, selector, path) stays positional. Named call sites read better, and new options can go anywhere in a signature without breaking existing callers. The full list is in the v2 upgrade guide. It covers the context helpers (`push_data`, `add_requests`, `block_requests`), `BasicCrawler.get_data`/`export_data`, `KeyValueStore.set_value` and the key iterators, `Dataset.export_to`, `ConcurrencySettings`, `RequestList`, `ProxyConfiguration.new_url`/`new_proxy_info`, the adaptive crawler factories and selector helpers, `HeaderGenerator.get_specific_headers`, `BrowserController.new_page`, `PlaywrightPersistentBrowser` and `find_links` on the parsers. `KeyValueStore.get_value` keeps `default_value` positional to match Crawlee JS. The three arguments of `new_proxy_info` now default to `None`, as on `new_url`. Closes: #1863 *✍️ Drafted by Claude Code*
1 parent 07bb164 commit 672c58b

30 files changed

Lines changed: 143 additions & 80 deletions

‎docs/examples/code_examples/adaptive_playwright_crawler.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async def request_handler_for_label(
2626
context.log.info(context.parsed_content.title)
2727

2828
# Locate element h2 within 5 seconds
29-
h2 = await context.query_selector_one('h2', timedelta(milliseconds=5000))
29+
h2 = await context.query_selector_one('h2', timeout=timedelta(milliseconds=5000))
3030
# Do stuff with element found by the selector
3131
context.log.info(h2)
3232

‎docs/guides/code_examples/http_crawlers/selectolax_parser.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def is_matching_selector(
4545

4646
@override
4747
def find_links(
48-
self, parsed_content: LexborHTMLParser, selector: str, attribute: str
48+
self, parsed_content: LexborHTMLParser, selector: str, *, attribute: str
4949
) -> Iterable[str]:
5050
"""Extract href attributes from elements matching the selector.
5151

‎docs/guides/code_examples/playwright_crawler_adaptive/handler.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ async def main() -> None:
1010
@crawler.router.default_handler
1111
async def request_handler(context: AdaptivePlaywrightCrawlingContext) -> None:
1212
# Locate element h2 within 5 seconds
13-
h2 = await context.query_selector_one('h2', timedelta(milliseconds=5000))
13+
h2 = await context.query_selector_one('h2', timeout=timedelta(milliseconds=5000))
1414
# Do stuff with element found by the selector
1515
context.log.info(h2)
1616

‎docs/upgrading/upgrading_to_v2.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,37 @@ We drop support for Python 3.10, which reached its end of life in October 2026.
2020
## `RequestQueueClient.is_finished` is abstract
2121

2222
`RequestQueueClient.is_finished` is now an abstract method. In v1, it fell back to `is_empty`, which ignores requests still being processed and could report the queue as finished too early. A subclass that doesn't implement it raises `TypeError` on instantiation. If you have a custom request queue client, implement `is_finished` so that it returns `True` only when the queue is empty and no requests are in progress. The built-in clients already implement it.
23+
24+
## Keyword-only arguments
25+
26+
Secondary arguments of the public API are now keyword-only. The main argument of each call, such as a key, a value, a selector or a path, stays positional. A call that passes any of the following arguments positionally raises `TypeError`, so pass them by name:
27+
28+
- `context.push_data(data, *, dataset_id, dataset_name, dataset_alias)`, and the same for `RequestHandlerRunResult.push_data`.
29+
- `context.add_requests(requests, *, rq_id, rq_name, rq_alias)`, and the same for `RequestHandlerRunResult.add_requests`.
30+
- `context.block_requests(*, url_patterns, extra_url_patterns)`, and `block_requests(page, *, url_patterns, extra_url_patterns)`.
31+
- `BasicCrawlingContext.create_modified_copy(*, push_data, add_requests, get_key_value_store)`.
32+
- `BasicCrawler.get_data(*, dataset_id, dataset_name, dataset_alias, ...)` and `BasicCrawler.export_data(path, *, dataset_id, dataset_name, dataset_alias, ...)`.
33+
- `KeyValueStore.set_value(key, value, *, content_type)`, also on the key-value store returned by `context.get_key_value_store()`.
34+
- `KeyValueStore.iterate_keys`, `iterate_values`, `iterate_entries` and `list_keys`, which take `exclusive_start_key` and `limit` by keyword only.
35+
- `Dataset.export_to(key, *, content_type, to_kvs_id, ...)`.
36+
- `ConcurrencySettings(*, min_concurrency, max_concurrency, max_tasks_per_minute, desired_concurrency)`.
37+
- `RequestList(requests, *, name, persist_state_key, persist_requests_key)`.
38+
- `ProxyConfiguration.new_url(*, session_id, request, proxy_tier)` and `ProxyConfiguration.new_proxy_info(*, session_id, request, proxy_tier)`. All three arguments of `new_proxy_info` now default to `None`.
39+
- `AdaptivePlaywrightCrawler.with_beautifulsoup_static_parser(*, ...)` and `with_parsel_static_parser(*, ...)`.
40+
- `AdaptivePlaywrightCrawlingContext.wait_for_selector`, `query_selector_one`, `query_selector_all` and `parse_with_static_parser`, which take `timeout` by keyword only.
41+
- `HeaderGenerator.get_specific_headers(header_names, *, browser_type)`.
42+
- `BrowserController.new_page(*, browser_new_context_options, proxy_info)`, including the Playwright and Stagehand controllers.
43+
- `PlaywrightPersistentBrowser(browser_type, user_data_dir, *, browser_launch_options)`.
44+
- `AbstractHttpParser.find_links(parsed_content, selector, *, attribute)`, including the built-in parsers. Custom parsers keep working, since the crawler passes `attribute` by keyword.
45+
46+
For example:
47+
48+
```python
49+
# v1
50+
await kvs.set_value('report', data, 'application/json')
51+
h2 = await context.query_selector_one('h2', timedelta(seconds=5))
52+
53+
# v2
54+
await kvs.set_value('report', data, content_type='application/json')
55+
h2 = await context.query_selector_one('h2', timeout=timedelta(seconds=5))
56+
```

‎src/crawlee/_types.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class ConcurrencySettings:
106106

107107
def __init__(
108108
self,
109+
*,
109110
min_concurrency: int = 1,
110111
max_concurrency: int = 100,
111112
max_tasks_per_minute: float = float('inf'),
@@ -223,6 +224,7 @@ async def set_value(
223224
self,
224225
key: str,
225226
value: Any,
227+
*,
226228
content_type: str | None = None,
227229
) -> None: ...
228230

@@ -242,6 +244,7 @@ async def set_value(
242244
self,
243245
key: str,
244246
value: Any,
247+
*,
245248
content_type: str | None = None,
246249
) -> None:
247250
self.updates[key] = KeyValueStoreValue(value, content_type)
@@ -278,6 +281,7 @@ def __init__(
278281
async def add_requests(
279282
self,
280283
requests: Sequence[str | Request],
284+
*,
281285
rq_id: str | None = None,
282286
rq_name: str | None = None,
283287
rq_alias: str | None = None,
@@ -294,6 +298,7 @@ async def add_requests(
294298
async def push_data(
295299
self,
296300
data: Sequence[Mapping[str, JsonSerializable]] | Mapping[str, JsonSerializable],
301+
*,
297302
dataset_id: str | None = None,
298303
dataset_name: str | None = None,
299304
dataset_alias: str | None = None,
@@ -336,6 +341,7 @@ class AddRequestsFunction(Protocol):
336341
def __call__(
337342
self,
338343
requests: Sequence[str | Request],
344+
*,
339345
rq_id: str | None = None,
340346
rq_name: str | None = None,
341347
rq_alias: str | None = None,
@@ -538,6 +544,7 @@ class PushDataFunction(Protocol):
538544
def __call__(
539545
self,
540546
data: Sequence[Mapping[str, JsonSerializable]] | Mapping[str, JsonSerializable],
547+
*,
541548
dataset_id: str | None = None,
542549
dataset_name: str | None = None,
543550
dataset_alias: str | None = None,
@@ -673,6 +680,7 @@ def __hash__(self) -> int:
673680

674681
def create_modified_copy(
675682
self,
683+
*,
676684
push_data: PushDataFunction | None = None,
677685
add_requests: AddRequestsFunction | None = None,
678686
get_key_value_store: GetKeyValueStoreFromRequestHandlerFunction | None = None,

‎src/crawlee/_utils/recoverable_state.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ async def persist_state(self, event_data: EventPersistStateData | None = None) -
192192
await self._key_value_store.set_value(
193193
self._persist_state_key,
194194
self._state.model_dump(mode='json', by_alias=True),
195-
'application/json',
195+
content_type='application/json',
196196
)
197197
else:
198198
self._log.debug('Persistence is not enabled - not doing anything')

‎src/crawlee/browsers/_browser_controller.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def browser_type(self) -> BrowserType:
7272
@abstractmethod
7373
async def new_page(
7474
self,
75+
*,
7576
browser_new_context_options: Mapping[str, Any] | None = None,
7677
proxy_info: ProxyInfo | None = None,
7778
) -> Page:

‎src/crawlee/browsers/_playwright_browser.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(
4040
self,
4141
browser_type: BrowserType,
4242
user_data_dir: str | Path | None,
43+
*,
4344
browser_launch_options: dict[str, Any],
4445
) -> None:
4546
self._browser_type = browser_type

‎src/crawlee/browsers/_playwright_browser_controller.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def browser_type(self) -> BrowserType:
157157
@override
158158
async def new_page(
159159
self,
160+
*,
160161
browser_new_context_options: Mapping[str, Any] | None = None,
161162
proxy_info: ProxyInfo | None = None,
162163
) -> Page:

‎src/crawlee/browsers/_playwright_browser_plugin.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ async def new_browser(self) -> PlaywrightBrowserController:
190190
if self._use_incognito_pages:
191191
browser: Browser | PlaywrightPersistentBrowser = await browser_type.launch(**self._browser_launch_options)
192192
else:
193-
browser = PlaywrightPersistentBrowser(browser_type, self._user_data_dir, self._browser_launch_options)
193+
browser = PlaywrightPersistentBrowser(
194+
browser_type, self._user_data_dir, browser_launch_options=self._browser_launch_options
195+
)
194196

195197
return PlaywrightBrowserController(
196198
browser,

0 commit comments

Comments
 (0)