Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cache
__pycache__
.mypy_cache
.ty_cache
.pytest_cache
.ruff_cache
.uv-cache
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ make format

### Type checking

Type checking is handled by [mypy](https://mypy.readthedocs.io/), verifying code against type annotations. Configuration settings can be found in `pyproject.toml`.
Type checking is handled by [ty](https://docs.astral.sh/ty/), verifying code against type annotations. Configuration settings can be found in `pyproject.toml`.

To run type checking:

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
INTEGRATION_TESTS_CONCURRENCY = 1

clean:
rm -rf .mypy_cache .pytest_cache .ruff_cache build dist htmlcov .coverage
rm -rf .ty_cache .pytest_cache .ruff_cache build dist htmlcov .coverage

install-dev:
uv sync --all-extras
Expand All @@ -23,7 +23,7 @@ lint:
uv run ruff check

type-check:
uv run mypy
uv run ty check

unit-tests:
uv run pytest \
Expand Down
2 changes: 1 addition & 1 deletion docs/02_concepts/code/05_custom_proxy_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async def custom_new_url_function(
async def main() -> None:
async with Actor:
proxy_cfg = await Actor.create_proxy_configuration(
new_url_function=custom_new_url_function, # type: ignore[arg-type]
new_url_function=custom_new_url_function, # ty: ignore[invalid-argument-type]
)

if not proxy_cfg:
Expand Down
2 changes: 1 addition & 1 deletion docs/02_concepts/code/07_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ async def main() -> None:
async with Actor:
# Create a webhook that will be triggered when the Actor run fails.
webhook = Webhook(
event_types=['ACTOR.RUN.FAILED'],
event_types=['ACTOR.RUN.FAILED'], # ty: ignore[invalid-argument-type]
request_url='https://example.com/run-failed',
)

Expand Down
5 changes: 2 additions & 3 deletions docs/02_concepts/code/07_webhook_preventing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ async def main() -> None:
async with Actor:
# Create a webhook that will be triggered when the Actor run fails.
webhook = Webhook(
event_types=['ACTOR.RUN.FAILED'],
event_types=['ACTOR.RUN.FAILED'], # ty: ignore[invalid-argument-type]
request_url='https://example.com/run-failed',
idempotency_key=Actor.configuration.actor_run_id,
)

# Add the webhook to the Actor.
await Actor.add_webhook(webhook)
await Actor.add_webhook(webhook, idempotency_key=Actor.configuration.actor_run_id)

# Raise an error to simulate a failed run.
raise RuntimeError('I am an error and I know it!')
Expand Down
43 changes: 13 additions & 30 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ dev = [
"crawlee[parsel]",
"dycw-pytest-only<3.0.0",
"griffe",
"mypy~=1.19.0",
"pre-commit<5.0.0",
"pydoc-markdown<5.0.0",
"pytest-asyncio<2.0.0",
Expand All @@ -78,6 +77,7 @@ dev = [
"pytest<9.0.0",
"ruff~=0.14.0",
"setuptools", # setuptools are used by pytest but not explicitly required
"ty~=0.0.0",
"types-cachetools<7.0.0",
"uvicorn[standard]",
"werkzeug<4.0.0", # Werkzeug is used by httpserver
Expand Down Expand Up @@ -195,37 +195,20 @@ asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
timeout = 1200

[tool.mypy]
python_version = "3.10"
plugins = ["pydantic.mypy"]
files = ["src", "tests", "docs", "website"]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_ignores = true
exclude = []

[[tool.mypy.overrides]]
module = [
'bs4', # Documentation
'httpx', # Documentation
'lazy_object_proxy', # Untyped and stubs not available
'playwright.*', # Documentation
'scrapy.*', # Untyped and stubs not available
'selenium.*', # Documentation
[tool.ty.environment]
python-version = "3.10"

[tool.ty.src]
include = ["src", "tests", "scripts", "docs", "website"]

[[tool.ty.overrides]]
include = [
"docs/**/*.py",
"website/**/*.py",
]
ignore_missing_imports = true

[tool.basedpyright]
pythonVersion = "3.10"
typeCheckingMode = "standard"
include = ["src", "tests", "docs", "website"]
[tool.ty.overrides.rules]
unresolved-import = "ignore"

[tool.coverage.report]
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "assert_never()"]
Expand Down
2 changes: 1 addition & 1 deletion src/apify/_charging.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ async def __aenter__(self) -> None:

# Load per-event pricing information
if pricing_info and pricing_info.pricing_model == 'PAY_PER_EVENT':
for event_name, event_pricing in pricing_info.pricing_per_event.actor_charge_events.items():
for event_name, event_pricing in pricing_info.pricing_per_event.actor_charge_events.items(): # ty:ignore[possibly-missing-attribute]
self._pricing_info[event_name] = PricingInfoItem(
price=event_pricing.event_price_usd,
title=event_pricing.event_title,
Expand Down
2 changes: 1 addition & 1 deletion src/apify/request_loaders/_apify_request_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ async def create_requests_from_response(request_input: _RequestsFromUrlInput, ta
)

get_response_task.add_done_callback(
lambda task, inp=remote_url_requests_input: asyncio.create_task( # type: ignore[misc]
lambda task, inp=remote_url_requests_input: asyncio.create_task(
create_requests_from_response(inp, task)
)
)
Expand Down
2 changes: 1 addition & 1 deletion src/apify/scrapy/_logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ def new_configure_logging(*args: Any, **kwargs: Any) -> None:
for logger_name in [None, *_ALL_LOGGERS]:
_configure_logger(logger_name, logging_level, handler)

scrapy_logging.configure_logging = new_configure_logging
scrapy_logging.configure_logging = new_configure_logging # ty: ignore[invalid-assignment]
4 changes: 2 additions & 2 deletions src/apify/scrapy/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def to_apify_request(scrapy_request: ScrapyRequest, spider: Spider) -> ApifyRequ
The converted Apify request if the conversion was successful, otherwise None.
"""
if not isinstance(scrapy_request, ScrapyRequest):
logger.warning('Failed to convert to Apify request: Scrapy request must be a ScrapyRequest instance.') # type: ignore[unreachable]
logger.warning('Failed to convert to Apify request: Scrapy request must be a ScrapyRequest instance.')
return None

logger.debug(f'to_apify_request was called (scrapy_request={scrapy_request})...')
Expand Down Expand Up @@ -58,7 +58,7 @@ def to_apify_request(scrapy_request: ScrapyRequest, spider: Spider) -> ApifyRequ
if isinstance(scrapy_request.headers, Headers):
request_kwargs['headers'] = HttpHeaders(dict(scrapy_request.headers.to_unicode_dict()))
else:
logger.warning( # type: ignore[unreachable]
logger.warning(
f'Invalid scrapy_request.headers type, not scrapy.http.headers.Headers: {scrapy_request.headers}'
)

Expand Down
20 changes: 10 additions & 10 deletions src/apify/storage_clients/_apify/_api_client_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ def get_resource_client(storage_id: str) -> KeyValueStoreClientAsync:
return apify_client.key_value_store(key_value_store_id=storage_id)

elif storage_type == 'RequestQueue':
collection_client = apify_client.request_queues() # type: ignore[assignment]
collection_client = apify_client.request_queues()
default_id = configuration.default_request_queue_id

def get_resource_client(storage_id: str) -> RequestQueueClientAsync: # type: ignore[misc]
def get_resource_client(storage_id: str) -> RequestQueueClientAsync:
# Use suitable client_key to make `hadMultipleClients` response of Apify API useful.
# It should persist across migrated or resurrected Actor runs on the Apify platform.
_api_max_client_key_length = 32
Expand All @@ -99,10 +99,10 @@ def get_resource_client(storage_id: str) -> RequestQueueClientAsync: # type: ig
return apify_client.request_queue(request_queue_id=storage_id, client_key=client_key)

elif storage_type == 'Dataset':
collection_client = apify_client.datasets() # type: ignore[assignment]
collection_client = apify_client.datasets()
default_id = configuration.default_dataset_id

def get_resource_client(storage_id: str) -> DatasetClientAsync: # type: ignore[misc]
def get_resource_client(storage_id: str) -> DatasetClientAsync:
return apify_client.dataset(dataset_id=storage_id)

else:
Expand All @@ -114,25 +114,25 @@ def get_resource_client(storage_id: str) -> DatasetClientAsync: # type: ignore[
case (None, None, None, None):
return await open_by_alias(
alias='__default__',
storage_type=storage_type, # type: ignore[arg-type]
storage_type=storage_type,
collection_client=collection_client,
get_resource_client_by_id=get_resource_client,
configuration=configuration,
)
) # ty:ignore[no-matching-overload]

# Open by alias.
case (str(), None, None, _):
return await open_by_alias(
alias=alias,
storage_type=storage_type, # type: ignore[arg-type]
storage_type=storage_type,
collection_client=collection_client,
get_resource_client_by_id=get_resource_client,
configuration=configuration,
)
) # ty:ignore[no-matching-overload]

# Open default storage.
case (None, None, None, str()):
resource_client = get_resource_client(default_id)
resource_client = get_resource_client(default_id) # ty: ignore[invalid-argument-type]
raw_metadata = await resource_client.get()
# Default storage does not exist. Create a new one.
if not raw_metadata:
Expand All @@ -147,7 +147,7 @@ def get_resource_client(storage_id: str) -> DatasetClientAsync: # type: ignore[

# Open by ID.
case (None, None, str(), _):
resource_client = get_resource_client(id)
resource_client = get_resource_client(id) # ty: ignore[invalid-argument-type]
raw_metadata = await resource_client.get()
if raw_metadata is None:
raise ValueError(f'Opening {storage_type} with id={id} failed.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ def _cache_request(
forefront: Whether the request was added to the forefront of the queue.
hydrated_request: The hydrated request object, if available.
"""
if processed_request.id is None:
raise ValueError('ProcessedRequest must have an ID to be cached.')

self._requests_cache[cache_key] = CachedRequest(
id=processed_request.id,
was_already_handled=processed_request.was_already_handled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def open(
key=configuration.input_key, record_path=input_file_path
)

return client
return client # ty: ignore[invalid-return-type]

@override
async def purge(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/actor/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ async def _make_actor(
# Get the source of main_func and convert it into a reasonable main_py file.
if main_func:
func_source = textwrap.dedent(inspect.getsource(main_func))
func_source = func_source.replace(f'def {main_func.__name__}(', 'def main(')
func_source = func_source.replace(f'def {main_func.__name__}(', 'def main(') # ty: ignore[unresolved-attribute]
main_py = '\n'.join( # noqa: FLY002
[
'import asyncio',
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/apify_api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def prepare_test_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callabl

def _prepare_test_env() -> None:
# Reset the Actor class state.
apify._actor.Actor.__wrapped__.__class__._is_any_instance_initialized = False # type: ignore[attr-defined]
apify._actor.Actor.__wrapped__.__class__._is_rebooting = False # type: ignore[attr-defined]
apify._actor.Actor.__wrapped__.__class__._is_any_instance_initialized = False # ty: ignore[unresolved-attribute]
apify._actor.Actor.__wrapped__.__class__._is_rebooting = False # ty: ignore[unresolved-attribute]
delattr(apify._actor.Actor, '__wrapped__')

# Set the environment variable for the local storage directory to the temporary path.
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/actor/test_actor_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def setup_mocked_charging(configuration: Configuration) -> AsyncGenerator[
mock_client.run = Mock(return_value=mock_run_client)

async with Actor(configuration):
charging_mgr_impl: ChargingManagerImplementation = Actor.get_charging_manager() # type: ignore[assignment]
charging_mgr_impl = Actor.get_charging_manager()

# Patch the charging manager to simulate running on Apify platform
with (
Expand All @@ -47,7 +47,7 @@ async def setup_mocked_charging(configuration: Configuration) -> AsyncGenerator[
patch.object(charging_mgr_impl, '_client', mock_client),
):
yield MockedChargingSetup(
charging_mgr=charging_mgr_impl,
charging_mgr=charging_mgr_impl, # ty: ignore[invalid-argument-type]
mock_charge=mock_charge,
mock_client=mock_client,
)
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/actor/test_actor_create_proxy_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def request_handler(request: Request, response: Response) -> Response:
assert proxy_configuration._password == DUMMY_PASSWORD
assert proxy_configuration._country_code == country_code

assert len(patched_apify_client.calls['user']['get']) == 1 # type: ignore[attr-defined]
assert len(patched_apify_client.calls['user']['get']) == 1 # ty: ignore[unresolved-attribute]
assert call_mock.call_count == 1

await Actor.exit()
Expand Down Expand Up @@ -146,7 +146,7 @@ def request_handler(request: Request, response: Response) -> Response:
== f'http://groups-{"+".join(groups)},country-{country_code}:{DUMMY_PASSWORD}@proxy.apify.com:8000'
)

assert len(patched_apify_client.calls['user']['get']) == 2 # type: ignore[attr-defined]
assert len(patched_apify_client.calls['user']['get']) == 2 # ty: ignore[unresolved-attribute]
assert call_mock.call_count == 2

await Actor.exit()
2 changes: 1 addition & 1 deletion tests/unit/actor/test_actor_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ async def handler(websocket: websockets.asyncio.server.ServerConnection) -> None
await websocket.wait_closed()

async with websockets.asyncio.server.serve(handler, host='localhost') as ws_server:
port: int = ws_server.sockets[0].getsockname()[1] # type: ignore[index]
port: int = ws_server.sockets[0].getsockname()[1] # ty: ignore[non-subscriptable]
monkeypatch.setenv(ActorEnvVars.EVENTS_WEBSOCKET_URL, f'ws://localhost:{port}')

mock_run_client = Mock()
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,16 @@ def make_httpserver() -> Iterator[HTTPServer]:
server = HTTPServer(threaded=True, host='127.0.0.1')
server.start()
yield server
server.clear() # type: ignore[no-untyped-call]
server.clear()
if server.is_running():
server.stop() # type: ignore[no-untyped-call]
server.stop()


@pytest.fixture
def httpserver(make_httpserver: HTTPServer) -> Iterator[HTTPServer]:
server = make_httpserver
yield server
server.clear() # type: ignore[no-untyped-call]
server.clear()


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/events/test_apify_event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async def handler(websocket: websockets.asyncio.server.ServerConnection) -> None
async with websockets.asyncio.server.serve(handler, host='localhost') as ws_server:
# When you don't specify a port explicitly, the websocket connection is opened on a random free port.
# We need to find out which port is that.
port: int = ws_server.sockets[0].getsockname()[1] # type: ignore[index]
port: int = ws_server.sockets[0].getsockname()[1] # ty: ignore[non-subscriptable]
monkeypatch.setenv(ActorEnvVars.EVENTS_WEBSOCKET_URL, f'ws://localhost:{port}')

async with ApifyEventManager(Configuration.get_global_configuration()):
Expand All @@ -175,7 +175,7 @@ async def send_platform_event(event_name: Event, data: Any = None) -> None:
async with websockets.asyncio.server.serve(handler, host='localhost') as ws_server:
# When you don't specify a port explicitly, the websocket connection is opened on a random free port.
# We need to find out which port is that.
port: int = ws_server.sockets[0].getsockname()[1] # type: ignore[index]
port: int = ws_server.sockets[0].getsockname()[1] # ty: ignore[non-subscriptable]
monkeypatch.setenv(ActorEnvVars.EVENTS_WEBSOCKET_URL, f'ws://localhost:{port}')

dummy_system_info = {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/scrapy/middlewares/test_apify_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,5 @@ def test_handles_exceptions(
dummy_request: Request,
exception: Exception,
) -> None:
returned_value = middleware.process_exception(dummy_request, exception, spider) # type: ignore[func-returns-value]
returned_value = middleware.process_exception(dummy_request, exception, spider)
assert returned_value is None
2 changes: 1 addition & 1 deletion tests/unit/scrapy/pipelines/test_actor_dataset_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ItemTestCase:
expected_exception=None,
),
ItemTestCase(
item=None, # type: ignore[arg-type] # that is the point of this test
item=None, # ty: ignore[invalid-argument-type]
item_dict={},
expected_exception=TypeError,
),
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/scrapy/requests/test_to_apify_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,5 @@ def test_with_id_and_unique_key(spider: Spider) -> None:
def test_invalid_scrapy_request_returns_none(spider: Spider) -> None:
scrapy_request = 'invalid_request'

apify_request = to_apify_request(scrapy_request, spider) # type: ignore[arg-type]
apify_request = to_apify_request(scrapy_request, spider) # ty: ignore[invalid-argument-type]
assert apify_request is None
Loading