Ctrl K
ComunidadGuías…

browser_use/legacy.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""0.x import paths and names 1.0 removed: each raises the removed error naming its 1.0 answer (MIGRATING.md).

Modules: a finder at the end of sys.meta_path, so it only sees paths no real module answers. Names in modules 1.0
kept (browser_use.agent.views.AgentOutput, ...): the module's __getattr__ calls removed_name()."""

from __future__ import annotations

import re
import sys
from importlib.machinery import ModuleSpec, PathFinder
from opcode import opmap

_LLM = (
	'from browser_use import ChatBrowserUse, ChatOpenAI, ChatAnthropic, ChatGoogle, or a Pydantic AI model string such as '
	"llm='openai:gpt-6-luna'."
)
CALL_MODEL = (
	'the agent calls the model itself. To call one directly use Pydantic AI: '
	'await pydantic_ai.Agent(llm.pydantic_model()).run(prompt) (output_type=YourModel for structured output).'
)
_LOOP = 'the 0.x loop internals are gone: the model writes Python cells, result.messages holds the transcript.'
_BROWSER = 'from browser_use import Browser; pass options to Browser(...) (BrowserSession and BrowserProfile are gone).'
_IN_PYTHON = 'the agent drives the page with Python and raw CDP in its REPL; there is no DOM, Page or Element API.'
_EVENTS = (
	"each step logs one INFO line to logging.getLogger('browser_use'); add a logging.Handler to stream progress, and "
	'read result.messages after the run.'
)
_MCP_CLIENT = 'call the MCP server from a plain @tools.action function.'
_MCP_SERVER = 'browser-use --cli-mcp (browser-use --mcp now serves the same browser_exec and browser_screenshot tools).'
_CODE_AGENT = 'from browser_use import Agent: the 1.0 agent writes Python itself.'
_CLI = 'the browser-use CLI pipes Python to browser-harness (browser-use --help), and browser-use --cli-mcp serves it over MCP.'
_USAGE = 'result.usage (tokens) and result.cost_usd.'
_LLM_ERROR = "result.error (run() returns status='error' for provider failures)."
_TELEMETRY = 'telemetry is internal; ANONYMIZED_TELEMETRY=false turns it off.'
_SKILLS = 'none (Cloud Skills were removed).'
_UTILS = 'none: it was a 0.x internal helper; copy what you need.'
_OBSERVABILITY = (
	"Pydantic AI's OpenTelemetry tracing: pydantic_ai.Agent.instrument_all() (or logfire.instrument_pydantic_ai()) "
	'traces every model request and tool call of every run.'
)


def _llm_modules() -> dict[str, str]:
	from browser_use import _LLM_REMOVED

	providers = {
		'aws': 'ChatAWSBedrock',
		'azure': 'ChatAzureOpenAI',
		'cerebras': 'ChatCerebras',
		'deepseek': 'ChatDeepSeek',
		'groq': 'ChatGroq',
		'litellm': 'ChatLiteLLM',
		'mistral': 'ChatMistral',
		'oci_raw': 'ChatOCIRaw',
		'ollama': 'ChatOllama',
		'openrouter': 'ChatOpenRouter',
		'orcarouter': 'ChatOrcaRouter',
		'vercel': 'ChatVercel',
	}
	kept = {'openai': 'ChatOpenAI', 'anthropic': 'ChatAnthropic', 'google': 'ChatGoogle', 'browser_use': 'ChatBrowserUse'}
	return {
		**{f'browser_use.llm.{m}': _LLM_REMOVED[cls] for m, cls in providers.items()},
		**{f'browser_use.llm.{m}': f'from browser_use import {cls}.' for m, cls in kept.items()},
	}


# 0.x module (and everything under it) -> the 1.0 answer. Longest match wins. Built on first use: it reads browser_use.
_MODULES: dict[str, str] = {}


def _modules() -> dict[str, str]:
	if not _MODULES:
		_MODULES.update(
			{
				'browser_use.actor': _IN_PYTHON,
				'browser_use.dom': _IN_PYTHON,
				'browser_use.agent.message_manager': _LOOP,
				'browser_use.agent.memory': 'none: every result stays in the conversation; long runs are compacted on their own.',
				'browser_use.agent.playwright_script_generator': _LOOP,
				'browser_use.agent.playwright_script_helpers': _LOOP,
				'browser_use.agent.tests': '',
				'browser_use.browser.tests': '',
				'browser_use.browser.context': _BROWSER,
				'browser_use.browser.types': _BROWSER,
				'browser_use.browser.utils': _BROWSER,
				'browser_use.browser.extensions': _BROWSER,
				'browser_use.browser.dolphin_service': "Browser(cdp_url=...) attaches to another browser's CDP endpoint.",
				'browser_use.code_use': _CODE_AGENT,
				'browser_use.skill_cli': _CLI,
				'browser_use.agent.variable_detector': _LOOP,
				'browser_use.agent.cloud_events': _LOOP,
				'browser_use.agent.prompts': 'Agent(extend_system_message=...) adds to the fixed system prompt.',
				'browser_use.agent.system_prompts': 'Agent(extend_system_message=...) adds to the fixed system prompt.',
				'browser_use.agent.gif': 'none (live_view=True shows a cloud browser live).',
				'browser_use.agent.judge': "result.success, the agent's own verdict.",
				'browser_use.beta': 'from browser_use import Agent.',
				'browser_use.browser.session': _BROWSER,
				'browser_use.browser.session_manager': _BROWSER,
				'browser_use.browser.profile': _BROWSER,
				'browser_use.browser.views': _BROWSER,
				'browser_use.browser.chrome': "Browser('chrome') (your open Chrome) or Browser('chromium') (a fresh one).",
				'browser_use.browser.cloud': 'Browser() is a cloud browser (cloud_profile_id=, cloud_proxy_country_code=, cloud_timeout=).',
				'browser_use.browser.events': _EVENTS,
				'browser_use.browser.watchdog_base': _BROWSER,
				'browser_use.browser.watchdogs': _BROWSER,
				**dict.fromkeys(  # 0.6.0 kept the watchdogs next to the session
					(
						f'browser_use.browser.{w}_watchdog'
						for w in (
							'aboutblank',
							'crash',
							'default_action',
							'dom',
							'downloads',
							'local_browser',
							'permissions',
							'popups',
							'screenshot',
							'security',
							'storage_state',
						)
					),
					_BROWSER,
				),
				'browser_use.browser.service': _BROWSER,  # 0.1.0
				'browser_use.browser._cdp_timeout': _BROWSER,
				'browser_use.browser.demo_mode': 'Agent(highlight_actions=True) marks each click and keypress on the page.',
				'browser_use.browser.python_highlights': 'Agent(highlight_actions=True) marks each click and keypress on the page.',
				'browser_use.browser.video_recorder': 'none (live_view=True shows a cloud browser live).',
				'browser_use.controller': 'from browser_use import Tools (Controller is the same class).',
				**dict.fromkeys(  # each 0.x submodule by name too: `from browser_use.tools import registry` asks for it
					(
						'browser_use.tools',
						*(f'browser_use.tools.{m}' for m in ('registry', 'service', 'views', 'extraction', 'utils')),
					),
					'from browser_use import Tools, ActionResult.',
				),
				'browser_use.filesystem': 'Agent(workspace=...) is a plain directory the agent reads and writes.',
				'browser_use.screenshots': 'the agent calls screenshot() in its REPL.',
				'browser_use.init_cmd': 'none (browser-use init templates were removed).',
				'browser_use.cli_templates': 'none (browser-use init templates were removed).',
				'browser_use.integrations': 'none yet: the hosted-browser integrations come back on the new Browser later.',
				'browser_use.llm': _LLM,
				**dict.fromkeys(
					(f'browser_use.llm.{m}' for m in ('messages', 'base', 'views', 'schema', 'exceptions')), CALL_MODEL
				),
				'browser_use.llm.models': "a Pydantic AI model string, e.g. llm='openai:gpt-6-luna'.",
				'browser_use.llm.tests': '',
				**_llm_modules(),
				'browser_use.mcp.client': _MCP_CLIENT,
				'browser_use.mcp.controller': _MCP_CLIENT,
				'browser_use.mcp.server': _MCP_SERVER,
				'browser_use.observability': _OBSERVABILITY,
				'browser_use.sandbox': '',
				'browser_use.skills.service': _SKILLS,
				'browser_use.skills.utils': _SKILLS,
				'browser_use.skills.views': _SKILLS,
				'browser_use.sync': 'none (cloud sync of local runs was removed).',
				'browser_use.tokens': _USAGE,
			}
		)
	return _MODULES


# names 0.x had in modules 1.0 kept -> the 1.0 answer (a name not listed is a plain AttributeError)
_AGENT_VIEWS = {
	**dict.fromkeys(('AgentOutput', 'AgentBrain'), 'the model writes Python cells; result.messages holds them.'),
	'AgentState': 'the result run() returns (AgentHistoryList): status, text, output, steps, usage, messages.',
	'AgentHistory': 'AgentHistoryList, the result run() returns; per-step detail is result.messages.',
	**dict.fromkeys(('AgentStepInfo', 'StepMetadata'), 'result.number_of_steps(), result.usage and result.messages.'),
	'AgentSettings': 'pass options to Agent(...).',
	'AgentError': "result.error (run() returns status='error' instead of raising).",
	'AgentStructuredOutput': 'Agent(output_model_schema=...); result.output holds the parsed value.',
	**dict.fromkeys(('JudgementResult', 'SimpleJudgeResult'), "result.success, the agent's own verdict."),
	**dict.fromkeys(('ModelPricingCatalog', 'Pricing', 'TokenDetails', 'TokenUsage'), _USAGE),
	'REQUIRED_LLM_API_ENV_VARS': "each model takes api_key=... or reads its provider's environment variable.",
	**dict.fromkeys(
		(
			'ActionLoopDetector',
			'DetectedVariable',
			'MessageCompactionSettings',
			'PageFingerprint',
			'PlanItem',
			'RerunSummaryAction',
			'ToolCallingMethod',
			'AgentOnlyAction',
			'AskHumanAgentAction',
			'ClickElementControllerHistoryItem',
			'InputTextControllerHistoryItem',
			'Output',
			'VariableMetadata',
			'compute_action_hash',
		),
		_LOOP,
	),
}
_NAMES: dict[str, dict[str, str]] = {
	'browser_use.agent': {'BetaAgentError': ''},
	'browser_use.agent.service': {
		'AgentHookFunc': _EVENTS,
		'AgentService': 'from browser_use import Agent.',  # 0.1.0
		'SKIP_LLM_API_KEY_VERIFICATION': 'none: a free request checks the key and model before the browser starts.',
		**dict.fromkeys(('Context', 'log_response'), _LOOP),
	},
	'browser_use.agent.views': _AGENT_VIEWS,
	'browser_use.browser': {
		'BrowserSession': 'Browser(...).',
		**dict.fromkeys(
			('BrowserProfile', 'BrowserConfig', 'BrowserContext', 'BrowserContextConfig'), 'pass options to Browser(...).'
		),
		'ProxySettings': "Browser(cloud_proxy_country_code='us') on cloud browsers; Browser('chromium') has no proxy yet.",
	},
	'browser_use.config': dict.fromkeys(
		(
			'AgentEntry',
			'BrowserProfileEntry',
			'DBStyleConfigJSON',
			'DBStyleEntry',
			'FlatEnvConfig',
			'LLMEntry',
			'OldConfig',
			'create_default_config',
			'get_default_llm',
			'get_default_profile',
			'load_and_migrate_config',
			'load_browser_use_config',
		),
		'Agent(...) and Browser(...) arguments; 1.0 reads no config.json (browser-use --mcp keeps its domain limits).',
	),
	'browser_use.exceptions': {'LLMException': _LLM_ERROR},
	'browser_use.llm': {
		**dict.fromkeys(
			(
				'AssistantMessage',
				'BaseMessage',
				'ContentImage',
				'ContentRefusal',
				'ContentText',
				'SystemMessage',
				'UserMessage',
			),
			CALL_MODEL,
		),
		'BaseChatModel': (
			'ChatBrowserUse, ChatOpenAI, ChatAnthropic, ChatGoogle, a Pydantic AI model string or a pydantic_ai.models.Model.'
		),
	},
	'browser_use.logging_config': dict.fromkeys(
		('FIFOHandler', 'addLoggingLevel', 'setup_log_pipes'),
		"logging.getLogger('browser_use') and BROWSER_USE_LOGGING_LEVEL=debug|info|warning|error.",
	),
	'browser_use.mcp': {
		'BrowserUseServer': _MCP_SERVER,
		**dict.fromkeys(('MCPClient', 'MCPToolWrapper'), _MCP_CLIENT),
	},
	'browser_use.skills': dict.fromkeys(('MissingCookieException', 'SkillService'), _SKILLS),
	'browser_use.cli': dict.fromkeys(
		(
			'auth',
			'BROWSER_LOGO',
			'BrowserUseApp',
			'CHROME_PROFILES_DIR',
			'get_default_config',
			'get_llm',
			'init',
			'INIT_TEMPLATES',
			'install',
			'load_user_config',
			'MAX_HISTORY_LENGTH',
			'RichLogHandler',
			'run_auth_command',
			'run_main_interface',
			'run_prompt_mode',
			'save_user_config',
			'setup_readline_history',
			'TEXTUAL_BORDER_STYLES',
			'textual_interface',
			'update_config_with_click_args',
			'USER_CONFIG_DIR',
			'USER_CONFIG_FILE',
			'USER_DATA_DIR',
		),
		_CLI,
	),
	**dict.fromkeys(
		('browser_use.telemetry', 'browser_use.telemetry.views'),
		dict.fromkeys(
			(
				'MCPClientTelemetryEvent',
				'MCPServerTelemetryEvent',
				'CLITelemetryEvent',
				'ControllerRegisteredFunctionsTelemetryEvent',
				'RegisteredFunction',
				'AgentEndTelemetryEvent',
				'AgentRunTelemetryEvent',
				'AgentStepErrorTelemetryEvent',
				'AgentStepTelemetryEvent',
			),
			_TELEMETRY,
		),
	),
	'browser_use.telemetry.service': {'xdg_cache_home': _TELEMETRY},
	'browser_use.utils': {
		**dict.fromkeys(
			(
				'SignalHandler',
				'GLOBAL_RETRY_SEMAPHORES',
				'GLOBAL_RETRY_SEMAPHORE_LOCK',
				'MULTIPROCESS_SEMAPHORES',
				'MULTIPROCESS_SEMAPHORE_DIR',
				'MULTIPROCESS_SEMAPHORE_LOCK',
				'retry',
				'BROWSER_USE_CONFIG_DIR',
				'handle_llm_error',
				'URL_NEGATION_PATTERN',
				'URL_PATTERN',
				'check_env_variables',
				'check_latest_browser_use_version',
				'collect_sensitive_data_values',
				'create_task_with_error_handling',
				'get_git_info',
				'has_url_negation',
				'is_new_tab_page',
				'is_placeholder_url',
				'is_unsafe_pattern',
				'match_url_with_domain_pattern',
				'merge_dicts',
				'redact_sensitive_string',
				'sanitize_surrogates',
				'sanitize_url_candidate',
				'time_execution_async',
				'time_execution_sync',
			),
			_UTILS,
		),
		'LLMException': _LLM_ERROR,
	},
}
# 0.1.x-0.5.x: from browser_use.browser.browser import ...
_NAMES['browser_use.browser.browser'] = {**_NAMES['browser_use.browser'], 'IN_DOCKER': _UTILS}


# Every public method and attribute of the 0.x BrowserSession, BrowserContext and Browser (0.1 to 0.13, from the tags)
# that 1.0's Browser does not have: each raises the removed error (browser.py REMOVED_METHODS names the common ones)
BROWSER_MEMBERS = frozenset(
	(
		'RECONNECT_WAIT_TIMEOUT',
		'add_highlights',
		'agent_current_page',
		'agent_focus',
		'agent_focus_target_id',
		'apply_session_overrides_to_profile',
		'attach_all_watchdogs',
		'auto_download_pdfs',
		'browser',
		'browser_context',
		'browser_pid',
		'cached_state',
		'cdp_client_for_frame',
		'cdp_client_for_node',
		'cdp_client_for_target',
		'cleanup_httpx_clients',
		'clear_cookies',
		'close_current_tab',
		'close_page',
		'cloud_browser',
		'connect',
		'context',
		'cookies',
		'copy_old_config_names_to_new',
		'create_new_tab',
		'current_page',
		'current_session_id',
		'current_target_id',
		'demo_mode',
		'detect_display_configuration',
		'downloaded_files',
		'execute_javascript',
		'find_file_input_near_element',
		'find_file_upload_element_by_index',
		'find_frame_target',
		'get_agent_current_page',
		'get_all_frames',
		'get_args',
		'get_browser_state_with_recovery',
		'get_current_page_title',
		'get_current_page_url',
		'get_current_target_info',
		'get_dom_element_at_coordinates',
		'get_dom_element_by_index',
		'get_element_by_index',
		'get_element_coordinates',
		'get_focused_target',
		'get_index_by_class',
		'get_index_by_id',
		'get_locate_element',
		'get_locate_element_by_css_selector',
		'get_locate_element_by_text',
		'get_locate_element_by_xpath',
		'get_minimal_state_summary',
		'get_most_recently_opened_target_id',
		'get_or_create_cdp_session',
		'get_page_info',
		'get_page_structure',
		'get_page_targets',
		'get_pages',
		'get_playwright_browser',
		'get_scroll_info',
		'get_selector_index',
		'get_selector_map',
		'get_session',
		'get_state',
		'get_state_as_text',
		'get_state_summary',
		'get_state_summary_with_fallback',
		'get_tabs_info',
		'get_target_id_from_session_id',
		'get_target_id_from_tab_id',
		'get_target_id_from_url',
		'go_back',
		'go_forward',
		'highlight_coordinate_click',
		'highlight_interaction_element',
		'human_current_page',
		'initialized',
		'is_cdp_connected',
		'is_connected',
		'is_file_input',
		'is_file_input_by_index',
		'is_file_uploader',
		'is_local',
		'is_reconnecting',
		'kwargs_for_connect',
		'kwargs_for_launch',
		'kwargs_for_launch_persistent_context',
		'kwargs_for_new_context',
		'llm_screenshot_size',
		'load_cookies_from_file',
		'load_storage_state',
		'logger',
		'must_get_current_page',
		'new_context',
		'new_tab',
		'on_AgentFocusChangedEvent',
		'on_BrowserStartEvent',
		'on_BrowserStopEvent',
		'on_CloseTabEvent',
		'on_FileDownloadedEvent',
		'on_NavigateToUrlEvent',
		'on_SwitchTabEvent',
		'on_TabClosedEvent',
		'on_TabCreatedEvent',
		'optimize_large_domain_lists',
		'playwright',
		'prepare_user_data_dir',
		'reconnect',
		'refresh',
		'refresh_page',
		'remove_highlights',
		'reset',
		'reset_context',
		'save_cookies',
		'save_storage_state',
		'screenshot_element',
		'send_demo_mode_log',
		'session_manager',
		'set_auto_download_pdfs',
		'set_browser_ownership',
		'set_extra_headers',
		'set_viewport_size',
		'setup_browser_via_browser_pid',
		'setup_browser_via_cdp_url',
		'setup_browser_via_passed_objects',
		'setup_browser_via_wss_url',
		'setup_new_browser_context',
		'setup_playwright',
		'switch_to_tab',
		'tabs',
		'update_cached_selector_map',
		'validate_highlight_elements_conflict',
		'validate_proxy_settings',
		'wait_for_element',
		'wait_if_captcha_solving',
		'warn_deterministic_rendering_weirdness',
		'warn_storage_state_user_data_dir_conflict',
		'warn_user_data_dir_non_default_version',
		'wss_url',
	)
)

# 0.x browser_use.llm built model instances on attribute access: llm.openai_gpt_4o, llm.google_gemini_2_5_flash, ...
_LLM_INSTANCE = re.compile(r'(openai|azure|google|anthropic|mistral|cerebras|bu)_\w+|codestral|pixtral_large|get_llm_by_name')
_LLM_INSTANCE_ANSWER = {
	'openai': "ChatOpenAI('<model>'), e.g. ChatOpenAI('gpt-4o') for openai_gpt_4o.",
	'azure': "llm='azure:<deployment>' with AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY and OPENAI_API_VERSION set.",
	'google': "ChatGoogle('<model>'), e.g. ChatGoogle('gemini-2.5-flash') for google_gemini_2_5_flash.",
	'anthropic': "ChatAnthropic('<model>'), e.g. ChatAnthropic('claude-sonnet-4-0') for anthropic_claude_sonnet_4_0.",
	'cerebras': "llm='cerebras:<model>' with CEREBRAS_API_KEY set.",
	'bu': "ChatBrowserUse('bu-fast') (the 0.x bu-1-0 / bu-2-0 / bu-latest need browser-use<1.0).",
}


def removed_name(module: str, name: str, names: dict[str, str] | None = None) -> ImportError | AttributeError:
	"""For a module's __getattr__: the removed error for a 0.x name (in _NAMES or names), else the plain AttributeError.
	An ImportError only for a from-import, where an AttributeError reads as a bare "cannot import name". Elsewhere an
	AttributeError with the same text: hasattr() and getattr(module, name, default) catch only that, and feature
	detection of a 0.x name (getattr(browser_use, 'BrowserSession', None)) raised instead of taking its fallback."""
	error = _removed_name(module, name, names)
	if isinstance(error, ImportError) and not _from_import():
		return AttributeError(str(error), name=name, obj=sys.modules.get(module))
	return error


def _removed_name(module: str, name: str, names: dict[str, str] | None) -> ImportError | AttributeError:
	if name in (names := {**_NAMES.get(module, {}), **(names or {})}):
		from browser_use.exceptions import removed  # here: this module is imported from modules exceptions imports

		return ImportError(removed(f'{module}.{name}', names[name]), name=module)
	if module == 'browser_use.llm' and _LLM_INSTANCE.fullmatch(name):
		from browser_use.exceptions import removed

		answer = _LLM_INSTANCE_ANSWER.get(name.split('_')[0], "llm='mistral:<model>' (pip install 'pydantic-ai-slim[mistral]').")
		if name == 'get_llm_by_name':
			answer = (
				"ChatOpenAI / ChatAnthropic / ChatGoogle / ChatBrowserUse, or a Pydantic AI model string: llm='openai:gpt-4o'."
			)
		return ImportError(removed(f'{module}.{name}', answer), name=module)
	if not name.startswith('_') and (message := _removed_submodule(module, name)):
		return ModuleNotFoundError(message, name=f'{module}.{name}')
	return AttributeError(f'module {module!r} has no attribute {name!r}')


def _from_import() -> bool:
	"""Whether the module attribute being looked up is read by a from-import: `from package import name` (importlib's
	_handle_fromlist asks hasattr(package, name)) or `from module import name` (the IMPORT_FROM instruction)."""
	frame = sys._getframe(1)
	# past removed_name and the module's __getattr__: the frame before them read the attribute
	while frame is not None and frame.f_code.co_name in ('removed_name', '__getattr__'):
		if not frame.f_globals.get('__name__', '').startswith('browser_use'):
			break
		frame = frame.f_back
	if frame is None:
		return False
	if frame.f_code.co_name == '_handle_fromlist' and frame.f_globals.get('__name__') == 'importlib._bootstrap':
		return True
	return frame.f_lasti >= 0 and frame.f_code.co_code[frame.f_lasti] == _IMPORT_FROM


_IMPORT_FROM = opmap['IMPORT_FROM']


def _removed_submodule(package: str, name: str) -> str | None:
	"""`from browser_use.browser import session`: from-import asks the package's __getattr__ before it imports the
	submodule, and swallows the finder's ModuleNotFoundError for a missing one ("cannot import name 'session'"). A
	real 1.0 module under a 0.x prefix (browser_use.tools) is None: the import system loads it."""
	fullname = f'{package}.{name}'
	if fullname not in _modules():  # a 0.x module itself, not a name under one (from browser_use.llm import NotAThing)
		return None
	if PathFinder.find_spec(fullname, getattr(sys.modules.get(package), '__path__', None) or []) is not None:
		return None
	return removed_module(fullname)


def removed_names(module: str):
	"""A module's __getattr__ that raises removed_name()."""

	def __getattr__(name: str):
		raise removed_name(module, name)

	return __getattr__


def removed_module(fullname: str) -> str | None:
	"""The removed error for a 0.x module path, or None for a path 0.x did not have."""
	from browser_use.exceptions import removed

	modules = _modules()
	parts = fullname.split('.')
	for i in range(len(parts), 1, -1):
		if (prefix := '.'.join(parts[:i])) in modules:
			return removed(fullname, modules[prefix])
	return None


class Removed0xModules:
	"""Last on sys.meta_path: a browser_use.* path no real module answered. A 0.x path raises ModuleNotFoundError (an
	ImportError, so `except ImportError` fallbacks still work) naming its 1.0 answer; any other path is left to Python."""

	@classmethod
	def find_spec(cls, fullname: str, path: object = None, target: object = None) -> ModuleSpec | None:
		if fullname.startswith('browser_use.') and (message := removed_module(fullname)):
			raise ModuleNotFoundError(message, name=fullname)
		return None


def install() -> None:
	if Removed0xModules not in sys.meta_path:
		sys.meta_path.append(Removed0xModules)  # type: ignore[arg-type]  # a finder class with find_spec, like PathFinder