Repository: apify/crawlee-python
File: src/crawlee/basic_crawler/basic_crawler.py (Python)

Code before:
        try:
            await run_task
        except CancelledError:
            pass
        finally:
            with suppress(NotImplementedError):
                asyncio.get_running_loop().remove_signal_handler(signal.SIGINT)

        if self._statistics.error_tracker.total > 0:
            self._logger.info(
                'Error analysis:'
                f' total_errors={self._statistics.error_tracker.total}'
                f' unique_errors={self._statistics.error_tracker.unique_error_count}'
            )

        if interrupted:
            self._logger.info(
                f'The crawl was interrupted. To resume, do: CRAWLEE_PURGE_ON_START=0 python {sys.argv[0]}'
            )

        self._running = False
        self._has_finished_before = True

        final_statistics = self._statistics.calculate()
        self._logger.info('Final request statistics: ')
        final_statistics.to_table()

        return final_statistics

    async def _run_crawler(self) -> None:
        async with AsyncExitStack() as exit_stack:
            await exit_stack.enter_async_context(self._event_manager)
            await exit_stack.enter_async_context(self._snapshotter)
            await exit_stack.enter_async_context(self._statistics)

            if self._use_session_pool:
                await exit_stack.enter_async_context(self._session_pool)

            for context_manager in self._additional_context_managers:
                await exit_stack.enter_async_context(context_manager)

            await self._pool.run()

    async def add_requests(
        self,
        requests: Sequence[str | BaseRequestData | Request],
        *,
        batch_size: int = 1000,
        wait_time_between_batches: timedelta = timedelta(0),
        wait_for_all_requests_to_be_added: bool = False,
        wait_for_all_requests_to_be_added_timeout: timedelta | None = None,

Review comment (suggestion):
The table should be concatenated with the logged message. What the `self._logger.info` call does depends on the configuration of the `logging` system, which is open to library users. Therefore, the two parts of the log line may end up in different places.

Code after:

        if requests is not None:
            await self.add_requests(requests)

        interrupted = False

        def sigint_handler() -> None:
            nonlocal interrupted

            if not interrupted:
                interrupted = True
                self._logger.info('Pausing... Press CTRL+C again to force exit.')

            run_task.cancel()

        run_task = asyncio.create_task(self._run_crawler())

        with suppress(NotImplementedError):  # event loop signal handlers are not supported on Windows
            asyncio.get_running_loop().add_signal_handler(signal.SIGINT, sigint_handler)

        try:
            await run_task
        except CancelledError:
            pass
        finally:
            with suppress(NotImplementedError):
                asyncio.get_running_loop().remove_signal_handler(signal.SIGINT)

        if self._statistics.error_tracker.total > 0:
            self._logger.info(
                'Error analysis:'
                f' total_errors={self._statistics.error_tracker.total}'
                f' unique_errors={self._statistics.error_tracker.unique_error_count}'
            )

        if interrupted:
            self._logger.info(
                f'The crawl was interrupted. To resume, do: CRAWLEE_PURGE_ON_START=0 python {sys.argv[0]}'
            )

        self._running = False
        self._has_finished_before = True

        final_statistics = self._statistics.calculate()
        self._logger.info(f'Final request statistics: {final_statistics.to_table()}')

        return final_statistics

    async def _run_crawler(self) -> None:
        async with AsyncExitStack() as exit_stack:
            await exit_stack.enter_async_context(self._event_manager)