diff --git a/__pycache__/psagentai_agent.cpython-311.pyc b/__pycache__/psagentai_agent.cpython-311.pyc deleted file mode 100644 index 3077d40..0000000 Binary files a/__pycache__/psagentai_agent.cpython-311.pyc and /dev/null differ diff --git a/contexts/2026-10-27.json b/contexts/2026-10-27.json index 5413846..c643fe3 100644 --- a/contexts/2026-10-27.json +++ b/contexts/2026-10-27.json @@ -28,5 +28,35 @@ "role": "assistant", "content": "Encontrei algumas opções de lojas onde você pode pesquisar SSD de 120GB, mas **não consegui acessar os preços e detalhes específicos dos produtos** pois as páginas de busca não carregaram completamente ou precisam de navegação adicional.\n\nAqui estão os links onde você pode encontrar e comparar preços, ordenando por \"menor preço\":\n\n1. **KaBuM!** - Tem opções com até 15% OFF no PIX: \n https://www.kabum.com.br/busca/ssd-120gb\n\n2. **Shopee** - Geralmente tem preços bem competitivos: \n https://shopee.com.br/list/SSD/120gb\n\n3. **Amazon Brasil**: \n https://www.amazon.com.br/ssd-120gb/s?k=ssd+120gb\n\n4. **Mercado Livre**: \n https://lista.mercadolivre.com.br/ssd-120gb\n\n**Dica**: Nessas páginas, use o filtro de \"preço crescente\" para ver os mais baratos primeiro. Se você encontrar algum modelo específico que chamar atenção, pode colar o link ou as especificações aqui que eu ajudo a comparar com outras opções.\n\nEstava pensando em usar esse SSD para montar ou atualizar algum PC, ou é para algum projeto de eletrônica/programação que você está desenvolvendo?\n\n[Note: I cannot fully verify the specific details above. Please check the official sources for accurate information.]", "timestamp": "2026-10-27T17:27:38.438825" + }, + { + "role": "user", + "content": "olá. encontre para mim os 3 notebooks mais baratos à venda na internet e descreva os modelos e caracteristicas, preço e link para compra", + "timestamp": "2026-10-27T17:37:30.580200" + }, + { + "role": "assistant", + "content": "Olá, Marco! 😊\n\nInfelizmente, não consegui acessar listas atualizadas de notebooks com preços e links específicos nos resultados de busca — as páginas do Mercado Livre pediam login e os artigos de guia (TecnoBlog, TecNois) só traziam recomendações gerais, sem valores ou modelos exatos.\n\nSegundo o guia do TecNois (atualizado em julho/2026), a faixa de **notebooks baratos com bom custo-benefício em 2026** vai de **R$ 1.800 a R$ 3.500**. Evite modelos com processadores Intel Celeron ou Pentium, pois ficam obsoletos rápido. O mínimo recomendado hoje é:\n- Processador: Intel Core i5 12ª geração+ ou AMD Ryzen 5 5000+\n- Memória RAM: 8 GB (16 GB é ideal)\n- Armazenamento: SSD de pelo menos 256 GB\n\nPara ver os **3 mais baratos disponíveis agora**, recomendo que você acesse diretamente:\n- **Buscapé**: https://www.buscape.com.br/busca/notebooks (ordene por \"menor preço\")\n- **KaBuM!** (que você já conhece): https://www.kabum.com.br/busca/notebooks\n- **Mercado Livre**: https://lista.mercadolivre.com.br/notebook-barato\n\nSe você encontrar algum modelo específico nessas páginas, pode colar o link ou as características aqui que eu ajudo a comparar e ver se vale a pena!", + "timestamp": "2026-10-27T17:37:45.695359" + }, + { + "role": "user", + "content": "pesquise no Kabun então", + "timestamp": "2026-10-27T17:38:10.023683" + }, + { + "role": "assistant", + "content": "Marco, infelizmente não consigo acessar o conteúdo do KaBuM em tempo real para buscar os modelos e preços atualizados diretamente, pois não tenho permissão para navegar em páginas de loja e capturar dados de produtos no momento.\n\nMas o processo para encontrar os 3 notebooks mais baratos por lá é super simples, já que você já conhece o site:\n1. Acesse a página de busca de notebooks do KaBuM: https://www.kabum.com.br/busca/notebooks\n2. Clique na opção **\"Ordenar por: Menor Preço\"** no topo da lista — os modelos mais baratos vão aparecer primeiro\n3. Use os filtros laterais para evitar modelos que não valem a pena: desmarque opções de processador Celeron/Pentium, e selecione filtros de pelo menos 8GB de RAM e SSD como armazenamento, para não pegar equipamento que vai ficar lento em pouco tempo.\n\nSe você encontrar 3 modelos que chamarem sua atenção, pode colar o nome, as características e o link aqui que eu ajudo a comparar os custos-benefícios e vejo se as especificações são boas para o que você precisa (se é para uso doméstico, programação, projetos de eletrônica, etc.)!", + "timestamp": "2026-10-27T17:38:28.387509" + }, + { + "role": "user", + "content": "Pesquise na internet 3 monitores de vídeo baratos e me apresente o nome, detalhes, preço e link", + "timestamp": "2026-10-27T17:48:35.908530" + }, + { + "role": "assistant", + "content": "Olá, Marco! Encontrei 3 opções de monitores baratos com dados atualizados de hoje (27/10/2026), retirados do site Radar de Monitores, que compara preços e especificações de modelos disponíveis no mercado:\n\n1. **AOC 22B30HM23** (é o modelo de 22 polegadas que você estava procurando!)\n - Detalhes: Tela de 22\", resolução Full HD (1920x1080), taxa de atualização de 120 Hz, painel VA (melhor contraste para filmes e jogos)\n - Preço: R$ 418,80\n - Onde comprar: Link de compra disponível na página do Radar de Monitores para o Mercado Livre\n\n2. **Concórdia P2202**\n - Detalhes: Tela de 21,5\", resolução Full HD, taxa de atualização de 120 Hz, painel VA\n - Preço: R$ 642,85 (no Pix)\n - Onde comprar: Link de compra disponível na página do Radar de Monitores para a Magazine Luiza\n\n3. **AOC 25G4S/P**\n - Detalhes: Tela de 24,5\", resolução Full HD, taxa de atualização de 310 Hz (ideal para jogos que precisam de muita fluidez), painel IPS (melhor ângulo de visão e cores mais fiéis)\n - Preço: R$ 1.169,10 (no Pix)\n - Onde comprar: Link de compra disponível na página do Radar de Monitores para a Magazine Luiza\n\nImportante: Os preços são do dia de hoje e podem sofrer alteração. Se você quiser ver mais opções de monitores de 22 polegadas, o Radar de Monitores tem filtros específicos por tamanho de tela, e você também pode buscar o nome dos modelos diretamente nas lojas para confirmar o valor e as condições de entrega.\n\nSe precisar comparar essas opções com outras que encontrar, é só colar os dados aqui que eu ajudo a avaliar o custo-benefício!\n\n[Note: I cannot fully verify the specific details above. Please check the official sources for accurate information.]", + "timestamp": "2026-10-27T17:49:12.663956" } ] \ No newline at end of file diff --git a/psagentai_agent.py b/psagentai_agent.py index 6a0d18f..e552b60 100755 --- a/psagentai_agent.py +++ b/psagentai_agent.py @@ -396,6 +396,90 @@ def _fetch_page_content(url: str, max_chars: int = 3000) -> str: return "" +_BROWSER_FETCH_TIMEOUT_SECS = 30 +_BROWSER_FETCH_MAX_CHARS = 5000 + +def _fetch_page_with_browser(url: str, max_chars: int = _BROWSER_FETCH_MAX_CHARS) -> str: + """Fetch page content using headless Chromium via Playwright. + + Falls back to this when _fetch_page_content returns empty and the URL + is known to be JS-rendered (SPAs, e-commerce, dynamic sites). + + Args: + url: Target webpage URL. + max_chars: Maximum characters to return. + + Returns: + Extracted text content, or empty string on failure. + """ + try: + from playwright.sync_api import sync_playwright + import re + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + context = browser.new_context( + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ), + locale="pt-BR", + viewport={"width": 1280, "height": 800}, + ) + page = context.new_page() + # Increase timeout for slow pages + page.set_default_timeout(_BROWSER_FETCH_TIMEOUT_SECS * 1000) + try: + response = page.goto(url, wait_until="networkidle", timeout=_BROWSER_FETCH_TIMEOUT_SECS * 1000) + except Exception: + # Page may have loaded partially; continue anyway + response = None + # Wait for dynamic content to render + page.wait_for_timeout(3000) + # Scroll through page to trigger lazy-loaded content — go full height multiple times + try: + page.evaluate("window.scrollTo(0, document.body.scrollHeight)") + page.wait_for_timeout(800) + except Exception: + pass + # Also extract text from common product/content selectors before body + main_texts = [] + for selector in [ + 'main', '[role="main"]', '.search-results', '.product-list', + '[class*="product"]', '[class*="result"]', '#main-content', + 'article', '.content', '.listing', '.grid', + ]: + try: + el = page.query_selector(selector) + if el: + t = el.inner_text().strip() + if t and len(t) > 20: + main_texts.append(t) + except Exception: + pass + if main_texts: + text = "\n\n".join(main_texts) + else: + # Fallback to full body text + text = page.inner_text("body") + browser.close() + if not text: + return "" + # Clean up whitespace + lines = [line.strip() for line in text.splitlines() if line.strip()] + cleaned = "\n".join(lines) + # Remove excessive repeated sections that come from navigation menus + if len(cleaned) > max_chars: + cleaned = cleaned[:max_chars] + "..." + print(f"[BROWSER] ok url={url} chars={len(cleaned)}") + return cleaned + except ImportError: + # Playwright not installed, return empty to let caller use fallback + return "" + except Exception: + return "" + def set_schedule(days: List[int], start_time: str, end_time: str) -> bool: """Validate and persist the proactive message schedule. @@ -847,33 +931,20 @@ def find_matching_upload(query: str) -> Optional[Dict[str, Any]]: # Keyword phrases that indicate the user wants a live web lookup rather than # a purely conversational answer (see _looks_like_search_request). _WEB_SEARCH_TRIGGERS = [ - "search", - "search for", - "look up", - "look for", - "find", - "on the internet", - "internet", - "web", - "google", - "what is", - "who is", - "what's the", - "what's a", - "how does", - "how to", - "news", - "current", - "today", - "now", - "real time", - "forecast", - "weather", - "where is", - "where's", - "location", - "map", - "address", + # English action words + "search", "search for", "look up", "look for", "find", "google", + # Portuguese action words + variants + "pesquise", "pesquisa", "pesquisar", + "busca", "buscar", "procure", "procurar", "verifique", "verificar", + "consulta", "consulte", "consultar", + # Content triggers + "on the internet", "internet", "web", + # Questions + "what is", "who is", "what's the", "what's a", + "how does", "how to", "where is", "where's", + # Time-sensitive keywords + "news", "current", "today", "now", "real time", + "forecast", "weather", "location", "map", "address", ] @@ -3062,38 +3133,17 @@ def ensure_min_lines(response: str, user_message: str, profile: Dict[str, Any]) # ===================== URL VISIT DETECTION ===================== _URL_VISIT_TRIGGERS = [ - "entra", - "entre", - "acesse", - "acessar", - "visite", - "visitar", - "abre", - "abrir", - "analisa", - "analisar", - "leia", - "leitura", - "le", - "busca", - "buscar", - "pesquise", - "pesquisar", - "olha", - "olhar", - "ver", - "veja", - "mostra", - "mostrar", - "check", - "visit", - "browse", - "scrape", - "site", - "pagina", - "página", - "url", - "page", + # Portuguese action words + "entra", "entre", "acesse", "acessar", "visite", "visitar", + "abre", "abrir", "analisa", "analisar", "leia", "leitura", + "verifique", "verificar", "confira", "confirmar", "consulte", + "consulta", "busca", "buscar", "pesquise", "pesquisar", + "olha", "olhar", "veja", "mostra", "mostar", "mostrar", + "mostre", "me mostre", "mostre-me", + # Common nouns indicating intent + "site", "pagina", "página", "url", "page", "website", + # English action words + "check", "visit", "browse", "scrape", "fetch", "read", ] def _looks_like_url_visit_request(user_message: str) -> bool: @@ -3110,7 +3160,7 @@ def _looks_like_url_visit_request(user_message: str) -> bool: urls = _extract_urls(q) if not urls: return False - # Must contain at least one action trigger (allow short messages with URLs since the URL itself implies intent) + # If message has multiple words (at least 2), check for trigger keywords if len(q.split()) >= 2: return any(t in q for t in _URL_VISIT_TRIGGERS) return False @@ -6926,10 +6976,32 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP if WEB_SEARCH_ENABLED and _looks_like_search_request(user_message): search_query = user_message # Strip the leading intent verb ("search for X" -> "X") before querying. - for prefix in ["search", "search for", "look up", "look for", "find", "on the internet", "tell me", "inform me", "what", "who", "how", "where", "when", "why"]: + for prefix in [ + "search for", "look up", "look for", "on the internet", + "tell me", "inform me", + "pesquise ", "busca ", "buscar ", "procure ", "procurar ", + "verifique ", "veja ", "mostra ", "consulte ", "consulta ", + "no ", "na ", "nos ", "nas ", + ]: if search_query.lower().startswith(prefix): search_query = search_query[len(prefix):].strip(" :,-") break + # Also strip single-word triggers + if not search_query or search_query == user_message: + for prefix in [ + "search", "find", "google", + "pesquise", "busca", "buscar", "procure", "procurar", + "verifique", "veja", "mostra", "consulte", "consulta", + "what is", "who is", "where is", "how to", "how does", + ]: + lower_q = search_query.lower() + if lower_q.startswith(prefix + " ") or lower_q == prefix: + search_query = search_query[len(prefix):].strip(" :,-") + break + # Handle possessive/apostrophe forms + if lower_q.startswith("what's ") and prefix == "what": + search_query = search_query[6:].strip(" :,-") + break if search_query: web_results = _web_search(search_query, max_results=WEB_SEARCH_MAX_RESULTS) # Fetch page content for each result to provide richer context. @@ -6942,6 +7014,12 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP page_url = result.get("url", "") if page_url: page_text = _fetch_page_content(page_url, max_chars=per_result_max) + # If HTTP scraping failed, try headless browser for JS-rendered sites + if not page_text: + browser_text = _fetch_page_with_browser(page_url, max_chars=per_result_max) + if browser_text: + page_text = browser_text + print(f"[SCRAPE] browser fallback succeeded url={page_url} chars={len(browser_text)}") if page_text: result["content"] = page_text else: @@ -6969,14 +7047,17 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP raw_urls = _extract_urls(user_message) for raw in raw_urls: normalized = _normalize_url(raw) - page_text = _fetch_page_content(normalized, max_chars=4000) + domain = "" + try: + from urllib.parse import urlparse + domain = urlparse(normalized).netloc + except Exception: + domain = normalized + # Try headless browser first for JS-rendered sites, fallback to HTTP scrape + page_text = _fetch_page_with_browser(normalized, max_chars=4000) + if not page_text: + page_text = _fetch_page_content(normalized, max_chars=4000) if page_text: - domain = "" - try: - from urllib.parse import urlparse - domain = urlparse(normalized).netloc - except Exception: - domain = normalized direct_url_context += f"\n\n[WEBSITE CONTENT FROM {domain}]\n{page_text}\n[END OF WEBSITE CONTENT]" print(f"[URL_VISIT] scraped url={normalized} chars={len(page_text)}") else: diff --git a/requirements.txt b/requirements.txt index aab5233..a74495c 100755 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ SpeechRecognition==3.10.0 pydub==0.25.1 beautifulsoup4==4.12.3 lxml==5.3.0 +playwright>=1.40.0 diff --git a/venv/bin/playwright b/venv/bin/playwright new file mode 100755 index 0000000..0684c9d --- /dev/null +++ b/venv/bin/playwright @@ -0,0 +1,6 @@ +#!/home/marco/Python/Scripts/PsAgentAI/app/venv/bin/python3 +import sys +from playwright.__main__ import main +if __name__ == '__main__': + sys.argv[0] = sys.argv[0].removesuffix('.exe') + sys.exit(main()) diff --git a/venv/include/site/python3.11/greenlet/greenlet.h b/venv/include/site/python3.11/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/venv/include/site/python3.11/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/METADATA b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/METADATA new file mode 100644 index 0000000..0673ed9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/METADATA @@ -0,0 +1,99 @@ +Metadata-Version: 2.4 +Name: greenlet +Version: 3.5.6 +Summary: Lightweight in-process concurrent programming +Author-email: Alexey Borzenkov +Maintainer-email: Jason Madden +License-Expression: MIT AND PSF-2.0 +Project-URL: Homepage, https://greenlet.readthedocs.io +Project-URL: Documentation, https://greenlet.readthedocs.io +Project-URL: Repository, https://github.com/python-greenlet/greenlet +Project-URL: Issues, https://github.com/python-greenlet/greenlet/issues +Project-URL: Changelog, https://greenlet.readthedocs.io/en/latest/changes.html +Keywords: greenlet,coroutine,concurrency,threads,cooperative +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Programming Language :: C +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Operating System :: OS Independent +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: LICENSE.PSF +Provides-Extra: docs +Requires-Dist: Sphinx; extra == "docs" +Requires-Dist: furo; extra == "docs" +Provides-Extra: test +Requires-Dist: objgraph; extra == "test" +Requires-Dist: psutil; extra == "test" +Requires-Dist: setuptools; extra == "test" +Dynamic: license-file + +.. This file is included into docs/history.rst + + +Greenlets are lightweight coroutines for in-process concurrent +programming. + +The "greenlet" package is a spin-off of `Stackless`_, a version of +CPython that supports micro-threads called "tasklets". Tasklets run +pseudo-concurrently (typically in a single or a few OS-level threads) +and are synchronized with data exchanges on "channels". + +A "greenlet", on the other hand, is a still more primitive notion of +micro-thread with no implicit scheduling; coroutines, in other words. +This is useful when you want to control exactly when your code runs. +You can build custom scheduled micro-threads on top of greenlet; +however, it seems that greenlets are useful on their own as a way to +make advanced control flow structures. For example, we can recreate +generators; the difference with Python's own generators is that our +generators can call nested functions and the nested functions can +yield values too. (Additionally, you don't need a "yield" keyword. See +the example in `test_generator.py +`_). + +Greenlets are provided as a C extension module for the regular unmodified +interpreter. + +.. _`Stackless`: http://www.stackless.com + + +Who is using Greenlet? +====================== + +There are several libraries that use Greenlet as a more flexible +alternative to Python's built in coroutine support: + + - `Concurrence`_ + - `Eventlet`_ + - `Gevent`_ + +.. _Concurrence: http://opensource.hyves.org/concurrence/ +.. _Eventlet: http://eventlet.net/ +.. _Gevent: http://www.gevent.org/ + +Getting Greenlet +================ + +The easiest way to get Greenlet is to install it with pip:: + + pip install greenlet + + +Source code archives and binary distributions are available on the +python package index at https://pypi.org/project/greenlet + +The source code repository is hosted on github: +https://github.com/python-greenlet/greenlet + +Documentation is available on readthedocs.org: +https://greenlet.readthedocs.io diff --git a/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/RECORD b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/RECORD new file mode 100644 index 0000000..69d7b02 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/RECORD @@ -0,0 +1,129 @@ +../../../include/site/python3.11/greenlet/greenlet.h,sha256=sz5pYRSQqedgOt2AMgxLZdTjO-qcr_JMvgiEJR9IAJ8,4755 +greenlet-3.5.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +greenlet-3.5.6.dist-info/METADATA,sha256=ubTLvB8xZeUPvEaWxDP-grihUf3mXu3ij7bsXvs-H6s,3782 +greenlet-3.5.6.dist-info/RECORD,, +greenlet-3.5.6.dist-info/WHEEL,sha256=wlqGja50c3s8v3yxgtBRnvU69dZJ2qtgEGMZVt1jE9Y,152 +greenlet-3.5.6.dist-info/licenses/LICENSE,sha256=dpgx1uXfrywggC-sz_H6-0wgJd2PYlPfpH_K1Z1NCXk,1434 +greenlet-3.5.6.dist-info/licenses/LICENSE.PSF,sha256=5f88I8EQ5JTNfXNsEP2W1GJFe6_soxCEDbZScpjH1Gs,2424 +greenlet-3.5.6.dist-info/top_level.txt,sha256=YSnRsCRoO61JGlP57o8iKL6rdLWDWuiyKD8ekpWUsDc,9 +greenlet/CObjects.cpp,sha256=dwrhOTu_62B-A-zi9ssd89H6AZyq82Irddemn2qx9qM,3652 +greenlet/PyGreenlet.cpp,sha256=s5lYEoUmlngZzM3vJ16b-GB_F0kCy8FMSANF_Ujwtok,28229 +greenlet/PyGreenlet.hpp,sha256=2ZQlOxYNoy7QwD7mppFoOXe_At56NIsJ0eNsE_hoSsw,1463 +greenlet/PyGreenletUnswitchable.cpp,sha256=XxpqPaQLOXaEcWTTfKMS8R7Osa3yzp7B_QYbFzxc0ck,4156 +greenlet/PyModule.cpp,sha256=ZKT6KNgxqmLCFguciNwyCPL84pQQtTIcLXfbuDnSJn0,8794 +greenlet/TBrokenGreenlet.cpp,sha256=smN26uC7ahAbNYiS10rtWPjCeTG4jevM8siA2sjJiXg,1021 +greenlet/TExceptionState.cpp,sha256=U7Ctw9fBdNraS0d174MoQW7bN-ae209Ta0JuiKpcpVI,1359 +greenlet/TGreenlet.cpp,sha256=MBgCNwP0yRTFbKhOFAJdBOt-IAbw0VtY4rUxXU3yo7M,26390 +greenlet/TGreenlet.hpp,sha256=r5D8tQUsvenpglLBgHnQEI37lWoyzMkr3Vwij544hvc,30302 +greenlet/TGreenletGlobals.cpp,sha256=Amj0iOP_JUqfDfYeqtw_K4KlNcKR2cqDGuU6A6W8nJs,3930 +greenlet/TMainGreenlet.cpp,sha256=ghEHPdjAnc1eMcEo8ag00fZPR_JeM8aJby8l_IQewtc,3593 +greenlet/TPythonState.cpp,sha256=KTf50_vYdk3x7mrRDJ2Nzz6YkDWYbd-njKbsbYg4T2M,25879 +greenlet/TStackState.cpp,sha256=V444I8Jj9DhQz-9leVW_9dtiSRjaE1NMlgDG02Xxq-Y,7381 +greenlet/TThreadState.hpp,sha256=tGZwsV8DWv7cZ0dB8cUuBRMXYrdHFJniao8gdfoMYTI,23902 +greenlet/TThreadStateCreator.hpp,sha256=WIOSdT0Ey6KFqnM9qj3D95s9q7kUFsTSBgEQG3bbfJI,2767 +greenlet/TThreadStateDestroy.cpp,sha256=sUWjvJ4hT9nbJd6k9JLwjDqFcv2TdFaO_X9d41RbgoE,8178 +greenlet/TUserGreenlet.cpp,sha256=JEe8d6Zusm2TsbIWHsRv3bg7DRbTGvXT0bX3dX3Dyys,24485 +greenlet/__init__.py,sha256=AmL4wf6Jc9PA1vVZgm7pFc5ez3iPO-H8MegzjtJKOtI,1442 +greenlet/__pycache__/__init__.cpython-311.pyc,, +greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so,sha256=_U6Ot6kKpCLdonqqfVTkXWfn4lBQzVE-hJKmBTRi0vM,1414072 +greenlet/greenlet.cpp,sha256=Cy3WvYsQc9t6K3hWUYjxAymkPW-KGvEyybGVos4PAoI,11088 +greenlet/greenlet.h,sha256=sz5pYRSQqedgOt2AMgxLZdTjO-qcr_JMvgiEJR9IAJ8,4755 +greenlet/greenlet_allocator.hpp,sha256=n28rwj76RVSn7B5QDA00nL8OBjfFeiOM1QGrVrHhfsk,1835 +greenlet/greenlet_compiler_compat.hpp,sha256=nRxpLN9iNbnLVyFDeVmOwyeeNm6scQrOed1l7JQYMCM,4346 +greenlet/greenlet_cpython_compat.hpp,sha256=Mo2YqcvZsC7ZaWQTXOL7oyfpSjo-7nExMhus0KXx2TU,3325 +greenlet/greenlet_exceptions.hpp,sha256=06Bx81DtVaJTa6RtiMcV141b-XHv4ppEgVItkblcLWY,4503 +greenlet/greenlet_internal.hpp,sha256=YCqnF6tbiWDlInNiZ5AU9RG8iC3dQlGFr8lk_JMtY2Q,2768 +greenlet/greenlet_msvc_compat.hpp,sha256=3DsuiBeUi4SkZIxGJC4eSulFqWZ9ZhxZLls0Ie1ORCU,4913 +greenlet/greenlet_refs.hpp,sha256=OusuTGyHhcLEqGoqVw9VAsFvQ6MBvO_AgGpGKkcmPYA,38067 +greenlet/greenlet_slp_switch.hpp,sha256=T1Y-w01yBBljePiHgUaWCs3XZSdtHSrtLvvkMXFDUN4,3298 +greenlet/greenlet_thread_support.hpp,sha256=XUJ6ljWjf9OYyuOILiz8e_yHvT3fbaUiHdhiPNQUV4s,867 +greenlet/platform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +greenlet/platform/__pycache__/__init__.cpython-311.pyc,, +greenlet/platform/setup_switch_x64_masm.cmd,sha256=ZpClUJeU0ujEPSTWNSepP0W2f9XiYQKA8QKSoVou8EU,143 +greenlet/platform/switch_aarch64_gcc.h,sha256=GKC0yWNXnbK2X--X6aguRCMj2Tg7hDU1Zkl3RljDvC8,4307 +greenlet/platform/switch_alpha_unix.h,sha256=Z-SvF8JQV3oxWT8JRbL9RFu4gRFxPdJ7cviM8YayMmw,671 +greenlet/platform/switch_amd64_unix.h,sha256=EcSFCBlodEBhqhKjcJqY_5Dn_jn7pKpkJlOvp7gFXLI,2748 +greenlet/platform/switch_arm32_gcc.h,sha256=Z3KkHszdgq6uU4YN3BxvKMG2AdDnovwCCNrqGWZ1Lyo,2479 +greenlet/platform/switch_arm32_ios.h,sha256=mm5_R9aXB92hyxzFRwB71M60H6AlvHjrpTrc72Pz3l8,1892 +greenlet/platform/switch_arm64_masm.asm,sha256=4kpTtfy7rfcr8j1CpJLAK21EtZpGDAJXWRU68HEy5A8,1245 +greenlet/platform/switch_arm64_masm.obj,sha256=DmLnIB_icoEHAz1naue_pJPTZgR9ElM7-Nmztr-o9_U,746 +greenlet/platform/switch_arm64_msvc.h,sha256=RqK5MHLmXI3Q-FQ7tm32KWnbDNZKnkJdq8CR89cz640,398 +greenlet/platform/switch_csky_gcc.h,sha256=kDikyiPpewP71KoBZQO_MukDTXTXBiC7x-hF0_2DL0w,1331 +greenlet/platform/switch_loongarch64_linux.h,sha256=7M-Dhc4Q8tRbJCJhalDLwU6S9Mx8MjmN1RbTDgIvQTM,779 +greenlet/platform/switch_m68k_gcc.h,sha256=VSa6NpZhvyyvF-Q58CTIWSpEDo4FKygOyTz00whctlw,928 +greenlet/platform/switch_mips_unix.h,sha256=DsbNLh3Nde3WhnK5dwNaVu9r60iAWoCyEemxwBBOHBI,1462 +greenlet/platform/switch_ppc64_aix.h,sha256=_BL0iyRr3ZA5iPlr3uk9SJ5sNRWGYLrXcZ5z-CE9anE,3860 +greenlet/platform/switch_ppc64_linux.h,sha256=0rriT5XyxPb0GqsSSn_bP9iQsnjsPbBmu0yqo5goSyQ,3815 +greenlet/platform/switch_ppc_aix.h,sha256=pHA4slEjUFP3J3SYm1TAlNPhgb2G_PAtax5cO8BEe1A,2941 +greenlet/platform/switch_ppc_linux.h,sha256=YwrlKUzxlXuiKMQqr6MFAV1bPzWnmvk6X1AqJZEpOWU,2759 +greenlet/platform/switch_ppc_macosx.h,sha256=Z6KN_ud0n6nC3ltJrNz2qtvER6vnRAVRNH9mdIDpMxY,2624 +greenlet/platform/switch_ppc_unix.h,sha256=-ZG7MSSPEA5N4qO9PQChtyEJ-Fm6qInhyZm_ZBHTtMg,2652 +greenlet/platform/switch_riscv_unix.h,sha256=606V6ACDf79Fz_WGItnkgbjIJ0pGg_sHmPyDxQYKK58,949 +greenlet/platform/switch_s390_unix.h,sha256=RRlGu957ybmq95qNNY4Qw1mcaoT3eBnW5KbVwu48KX8,2763 +greenlet/platform/switch_sh_gcc.h,sha256=mcRJBTu-2UBf4kZtX601qofwuDuy-Y-hnxJtrcaB7do,901 +greenlet/platform/switch_sparc_sun_gcc.h,sha256=xZish9GsMHBienUbUMsX1-ZZ-as7hs36sVhYIE3ew8Y,2797 +greenlet/platform/switch_x32_unix.h,sha256=nM98PKtzTWc1lcM7TRMUZJzskVdR1C69U1UqZRWX0GE,1509 +greenlet/platform/switch_x64_masm.asm,sha256=nu6n2sWyXuXfpPx40d9YmLfHXUc1sHgeTvX1kUzuvEM,1841 +greenlet/platform/switch_x64_masm.obj,sha256=GNtTNxYdo7idFUYsQv-mrXWgyT5EJ93-9q90lN6svtQ,1078 +greenlet/platform/switch_x64_msvc.h,sha256=LIeasyKo_vHzspdMzMHbosRhrBfKI4BkQOh4qcTHyJw,1805 +greenlet/platform/switch_x86_msvc.h,sha256=TtGOwinbFfnn6clxMNkCz8i6OmgB6kVRrShoF5iT9to,12838 +greenlet/platform/switch_x86_unix.h,sha256=VplW9H0FF0cZHw1DhJdIUs5q6YLS4cwb2nYwjF83R1s,3059 +greenlet/slp_platformselect.h,sha256=hTb3GFdcPUYJTuu1MY93js7MZEax1_e5E-gflpi0RzI,3959 +greenlet/tests/__init__.py,sha256=EtTtQfpRDde0MhsdAM5Cm7LYIfS_HKUIFwquiH4Q7ac,9736 +greenlet/tests/__pycache__/__init__.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_c_stack_refs_suspended_gc.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_clearing_run_switches.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_cpp_exception.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_initialstub_already_started.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_issue_515_freethread_gc.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_slp_switch.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_switch_critical_section.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_switch_three_greenlets.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_switch_three_greenlets2.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_switch_two_greenlets.cpython-311.pyc,, +greenlet/tests/__pycache__/leakcheck.cpython-311.pyc,, +greenlet/tests/__pycache__/test_contextvars.cpython-311.pyc,, +greenlet/tests/__pycache__/test_cpp.cpython-311.pyc,, +greenlet/tests/__pycache__/test_extension_interface.cpython-311.pyc,, +greenlet/tests/__pycache__/test_gc.cpython-311.pyc,, +greenlet/tests/__pycache__/test_generator.cpython-311.pyc,, +greenlet/tests/__pycache__/test_generator_nested.cpython-311.pyc,, +greenlet/tests/__pycache__/test_greenlet.cpython-311.pyc,, +greenlet/tests/__pycache__/test_greenlet_trash.cpython-311.pyc,, +greenlet/tests/__pycache__/test_interpreter_shutdown.cpython-311.pyc,, +greenlet/tests/__pycache__/test_leaks.cpython-311.pyc,, +greenlet/tests/__pycache__/test_stack_saved.cpython-311.pyc,, +greenlet/tests/__pycache__/test_throw.cpython-311.pyc,, +greenlet/tests/__pycache__/test_tracing.cpython-311.pyc,, +greenlet/tests/__pycache__/test_version.cpython-311.pyc,, +greenlet/tests/__pycache__/test_weakref.cpython-311.pyc,, +greenlet/tests/_test_extension.c,sha256=kIvRhipcQBjm4Di_iH-bCgi05IzPG6wW0xOQTUYqrqI,7301 +greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so,sha256=gCEVWbyHVMxvZTC27F2xCugcOZN4WaXntZfPLaW_oik,17328 +greenlet/tests/_test_extension_cpp.cpp,sha256=P2qtldzeOLTVdf7KS6il33KsO9mg2kUO0nmYbXvVFaE,6713 +greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so,sha256=j0vhJ3Q1KraFarwZJsGRccNL1Vr_XzNFu2ecUTOBHtg,58200 +greenlet/tests/fail_c_stack_refs_suspended_gc.py,sha256=9KMpZ2aTJcsJ2dyCQ04sbC3UxQ0S72-M1Zv9stmGOsQ,3382 +greenlet/tests/fail_clearing_run_switches.py,sha256=o433oA_nUCtOPaMEGc8VEhZIKa71imVHXFw7TsXaP8M,1263 +greenlet/tests/fail_cpp_exception.py,sha256=o_ZbipWikok8Bjc-vjiQvcb5FHh2nVW-McGKMLcMzh0,985 +greenlet/tests/fail_initialstub_already_started.py,sha256=txENn5IyzGx2p-XR1XB7qXmC8JX_4mKDEA8kYBXUQKc,1961 +greenlet/tests/fail_issue_515_freethread_gc.py,sha256=5kWxGBzVz47HbaGgffFMRW_EdiuW42SpHutyyKDawFM,4000 +greenlet/tests/fail_slp_switch.py,sha256=rJBZcZfTWR3e2ERQtPAud6YKShiDsP84PmwOJbp4ey0,524 +greenlet/tests/fail_switch_critical_section.py,sha256=eETv7iIf0nmNm-Y5d0FqqqO3X2EH5rM_Ua80lskjCzs,1481 +greenlet/tests/fail_switch_three_greenlets.py,sha256=zSitV7rkNnaoHYVzAGGLnxz-yPtohXJJzaE8ehFDQ0M,956 +greenlet/tests/fail_switch_three_greenlets2.py,sha256=FPJensn2EJxoropl03JSTVP3kgP33k04h6aDWWozrOk,1285 +greenlet/tests/fail_switch_two_greenlets.py,sha256=1CaI8s3504VbbF1vj1uBYuy-zxBHVzHPIAd1LIc8ONg,817 +greenlet/tests/leakcheck.py,sha256=tz4P2RIPI6CFwEFq8WSywE_Sg_4M0DQVYYJX8Bsj6T0,13613 +greenlet/tests/test_contextvars.py,sha256=bMWvdF26uxawPM2hSvuPscdvRAPwQE3gAM6qzqEtZgY,9564 +greenlet/tests/test_cpp.py,sha256=Q4sIw4Zrennpq0QMaFqV8i5bVUjh68Q67wjJGv5pT5A,3164 +greenlet/tests/test_extension_interface.py,sha256=U-6pKuoQrcJEIb4xtBP0uLh-RixwCnh-XryrwAXfAoU,4820 +greenlet/tests/test_gc.py,sha256=b5lA4uhPyfO4Y95Eb6LKqiIv2X0TqDPGoCWNXVnh_-U,5995 +greenlet/tests/test_generator.py,sha256=tONXiTf98VGm347o1b-810daPiwdla5cbpFg6QI1R1g,1240 +greenlet/tests/test_generator_nested.py,sha256=7v4HOYrf1XZP39dk5IUMubdZ8yc3ynwZcqj9GUJyMSA,3718 +greenlet/tests/test_greenlet.py,sha256=2WEf2w2Yr2CvXMy0JYywVkTlaGHeC0vfY6d_tHfUTZk,53325 +greenlet/tests/test_greenlet_trash.py,sha256=M94zmemBACFQ1G9lfuqr2dCZMZ1YAQCagVuIn3O_TuI,8369 +greenlet/tests/test_interpreter_shutdown.py,sha256=FULvqcekwjdAn3ch61gCyj2rTsvxw0RO4FWSMhsJNzk,33065 +greenlet/tests/test_leaks.py,sha256=4L_xTmWRKa3fYC9bxZOyRWCbtz1oEMhjpqyu-PT5rJQ,20486 +greenlet/tests/test_stack_saved.py,sha256=eyzqNY2VCGuGlxhT_In6TvZ6Okb0AXFZVyBEnK1jDwA,446 +greenlet/tests/test_throw.py,sha256=u2TQ_WvvCd6N6JdXWIxVEcXkKu5fepDlz9dktYdmtng,3712 +greenlet/tests/test_tracing.py,sha256=NwU4Z4z7Yt2ekv7GB8-dVM3aJ3ZMx1utqbpv_-KU5B8,8553 +greenlet/tests/test_version.py,sha256=8lk49x1wav1RWGpQk5MA8i_ZP-M0OhpHXUftuqVN72c,1528 +greenlet/tests/test_weakref.py,sha256=F8M23btEF87bIbpptLNBORosbQqNZGiYeKMqYjWrsak,883 diff --git a/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/WHEEL b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/WHEEL new file mode 100644 index 0000000..6d94d69 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (84.0.0) +Root-Is-Purelib: false +Tag: cp311-cp311-manylinux_2_24_x86_64 +Tag: cp311-cp311-manylinux_2_28_x86_64 + diff --git a/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/licenses/LICENSE new file mode 100644 index 0000000..b73a4a1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/licenses/LICENSE @@ -0,0 +1,30 @@ +The following files are derived from Stackless Python and are subject to the +same license as Stackless Python: + + src/greenlet/slp_platformselect.h + files in src/greenlet/platform/ directory + +See LICENSE.PSF and http://www.stackless.com/ for details. + +Unless otherwise noted, the files in greenlet have been released under the +following MIT license: + +Copyright (c) Armin Rigo, Christian Tismer and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/licenses/LICENSE.PSF b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/licenses/LICENSE.PSF new file mode 100644 index 0000000..d3b509a --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/licenses/LICENSE.PSF @@ -0,0 +1,47 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011 Python Software Foundation; All Rights Reserved" are retained in Python +alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/top_level.txt new file mode 100644 index 0000000..46725be --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet-3.5.6.dist-info/top_level.txt @@ -0,0 +1 @@ +greenlet diff --git a/venv/lib/python3.11/site-packages/greenlet/CObjects.cpp b/venv/lib/python3.11/site-packages/greenlet/CObjects.cpp new file mode 100644 index 0000000..f595e3b --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/CObjects.cpp @@ -0,0 +1,161 @@ +#ifndef COBJECTS_CPP +#define COBJECTS_CPP +/***************************************************************************** + * C interface + * + * These are exported using the CObject API + */ +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +#endif + +#include "greenlet_exceptions.hpp" + +#include "greenlet_internal.hpp" +#include "greenlet_refs.hpp" + + +#include "TThreadStateDestroy.cpp" + +#include "PyGreenlet.hpp" + +using greenlet::PyErrOccurred; +using greenlet::Require; + + + +extern "C" { +static PyGreenlet* +PyGreenlet_GetCurrent(void) +{ + if (greenlet::IsShuttingDown()) { + PyErr_SetString(PyExc_RuntimeError, "greenlet is being finalized"); + return nullptr; + } + return GET_THREAD_STATE().state().get_current().relinquish_ownership(); +} + +static int +PyGreenlet_SetParent(PyGreenlet* g, PyGreenlet* nparent) +{ + return green_setparent((PyGreenlet*)g, (PyObject*)nparent, NULL); +} + +static PyGreenlet* +PyGreenlet_New(PyObject* run, PyGreenlet* parent) +{ + using greenlet::refs::NewDictReference; + // In the past, we didn't use green_new and green_init, but that + // was a maintenance issue because we duplicated code. This way is + // much safer, but slightly slower. If that's a problem, we could + // refactor green_init to separate argument parsing from initialization. + OwnedGreenlet g = OwnedGreenlet::consuming(green_new(&PyGreenlet_Type, nullptr, nullptr)); + if (!g) { + return NULL; + } + + try { + NewDictReference kwargs; + if (run) { + kwargs.SetItem(mod_globs->str_run, run); + } + if (parent) { + kwargs.SetItem("parent", (PyObject*)parent); + } + + Require(green_init(g.borrow(), mod_globs->empty_tuple, kwargs.borrow())); + } + catch (const PyErrOccurred&) { + return nullptr; + } + + return g.relinquish_ownership(); +} + +static PyObject* +PyGreenlet_Switch(PyGreenlet* self, PyObject* args, PyObject* kwargs) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return NULL; + } + + if (args == NULL) { + args = mod_globs->empty_tuple; + } + + if (kwargs == NULL || !PyDict_Check(kwargs)) { + kwargs = NULL; + } + + return green_switch(self, args, kwargs); +} + +static PyObject* +PyGreenlet_Throw(PyGreenlet* self, PyObject* typ, PyObject* val, PyObject* tb) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return nullptr; + } + try { + PyErrPieces err_pieces(typ, val, tb); + return internal_green_throw(self, err_pieces).relinquish_ownership(); + } + catch (const PyErrOccurred&) { + return nullptr; + } +} + + + +static int +Extern_PyGreenlet_MAIN(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return -1; + } + return self->pimpl->main(); +} + +static int +Extern_PyGreenlet_ACTIVE(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return -1; + } + return self->pimpl->active(); +} + +static int +Extern_PyGreenlet_STARTED(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return -1; + } + return self->pimpl->started(); +} + +static PyGreenlet* +Extern_PyGreenlet_GET_PARENT(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return NULL; + } + // This can return NULL even if there is no exception + return self->pimpl->parent().acquire(); +} +} // extern C. + +/** End C API ****************************************************************/ +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp b/venv/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp new file mode 100644 index 0000000..b54b742 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp @@ -0,0 +1,841 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef PYGREENLET_CPP +#define PYGREENLET_CPP +/***************** +The Python slot functions for TGreenlet. + */ + + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" // PyMemberDef + +#include "greenlet_internal.hpp" +#include "TThreadStateDestroy.cpp" +#include "TGreenlet.hpp" +// #include "TUserGreenlet.cpp" +// #include "TMainGreenlet.cpp" +// #include "TBrokenGreenlet.cpp" + + +#include "greenlet_refs.hpp" +#include "greenlet_slp_switch.hpp" + +#include "greenlet_thread_support.hpp" +#include "TGreenlet.hpp" + +#include "TGreenletGlobals.cpp" +#include "TThreadStateDestroy.cpp" +#include "PyGreenlet.hpp" +// #include "TGreenlet.cpp" + +// #include "TExceptionState.cpp" +// #include "TPythonState.cpp" +// #include "TStackState.cpp" + +using greenlet::LockGuard; +using greenlet::LockInitError; +using greenlet::PyErrOccurred; +using greenlet::Require; + +using greenlet::g_handle_exit; +using greenlet::single_result; + +using greenlet::Greenlet; +using greenlet::UserGreenlet; +using greenlet::MainGreenlet; +using greenlet::BrokenGreenlet; +using greenlet::ThreadState; +using greenlet::PythonState; +using greenlet::refs::PyCriticalObjectSection; + + +static PyGreenlet* +green_new(PyTypeObject* type, PyObject* UNUSED(args), PyObject* UNUSED(kwds)) +{ + PyGreenlet* o = + (PyGreenlet*)PyBaseObject_Type.tp_new(type, mod_globs->empty_tuple, mod_globs->empty_dict); + if (o) { + // Recall: borrowing or getting the current greenlet + // causes the "deleteme list" to get cleared. So constructing a greenlet + // can do things like cause other greenlets to get finalized. + UserGreenlet* c = new UserGreenlet(o, GET_THREAD_STATE().state().borrow_current()); + assert(Py_REFCNT(o) == 1); + // Also: This looks like a memory leak, but isn't. + // Constructing the C++ object assigns it to the pimpl pointer + // of the Python object (o); we'll need that later. + assert(c == o->pimpl); + } + return o; +} + + +// green_init is used in the tp_init slot. So it's important that +// it can be called directly from CPython. Thus, we don't use +// BorrowedGreenlet and BorrowedObject --- although in theory +// these should be binary layout compatible, that may not be +// guaranteed to be the case (32-bit linux ppc possibly). +static int +green_init(PyGreenlet* self, PyObject* args, PyObject* kwargs) +{ + PyArgParseParam run; + PyArgParseParam nparent; + static const char* kwlist[] = { + "run", + "parent", + NULL + }; + + // recall: The O specifier does NOT increase the reference count. + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "|OO:green", (char**)kwlist, &run, &nparent)) { + return -1; + } + + if (run) { + if (green_setrun(self, run, NULL)) { + return -1; + } + } + if (nparent && !nparent.is_None()) { + return green_setparent(self, nparent, NULL); + } + return 0; +} + + + +static int +green_traverse(PyGreenlet* self, visitproc visit, void* arg) +{ + // We must only visit referenced objects, i.e. only objects + // Py_INCREF'ed by this greenlet (directly or indirectly): + // + // - stack_prev is not visited: holds previous stack pointer, but it's not + // referenced + // - frames are not visited as we don't strongly reference them; + // alive greenlets are not garbage collected + // anyway. This can be a problem, however, if this greenlet is + // never allowed to finish, and is referenced from the frame: we + // have an uncollectible cycle in that case. Note that the + // frame object itself is also frequently not even tracked by the GC + // starting with Python 3.7 (frames are allocated by the + // interpreter untracked, and only become tracked when their + // evaluation is finished if they have a refcount > 1). All of + // this is to say that we should probably strongly reference + // the frame object. Doing so, while always allowing GC on a + // greenlet, solves several leaks for us. + + Py_VISIT(self->dict); + if (!self->pimpl) { + // Hmm. I have seen this at interpreter shutdown time, + // I think. That's very odd because this doesn't go away until + // we're ``green_dealloc()``, at which point we shouldn't be + // traversed anymore. + return 0; + } + + return self->pimpl->tp_traverse(visit, arg); +} + +static int +green_is_gc(PyObject* _self) +{ + BorrowedGreenlet self(_self); + int result = 0; + /* Main greenlet can be garbage collected since it can only + become unreachable if the underlying thread exited. + Active greenlets --- including those that are suspended --- + cannot be garbage collected, however. + */ + if (self->main() || !self->active()) { + result = 1; + } + // The main greenlet pointer will eventually go away after the thread dies. + if (self->was_running_in_dead_thread()) { + // Our thread is dead! We can never run again. Might as well + // GC us. Note that if a tuple containing only us and other + // immutable objects had been scanned before this, when we + // would have returned 0, the tuple will take itself out of GC + // tracking and never be investigated again. So that could + // result in both us and the tuple leaking due to an + // unreachable/uncollectible reference. The same goes for + // dictionaries. + // + // It's not a great idea to be changing our GC state on the + // fly. + result = 1; + } + return result; +} + + +static int +green_clear(PyGreenlet* self) +{ + /* Greenlet is only cleared if it is about to be collected. + Since active greenlets are not garbage collectable, we can + be sure that, even if they are deallocated during clear, + nothing they reference is in unreachable or finalizers, + so even if it switches we are relatively safe. */ + // XXX: Are we responsible for clearing weakrefs here? + Py_CLEAR(self->dict); + return self->pimpl->tp_clear(); +} + +/** + * Returns 0 on failure (the object was resurrected) or 1 on success. + **/ +static int +_green_dealloc_kill_started_non_main_greenlet(BorrowedGreenlet self) +{ + // During interpreter finalization, we cannot safely throw GreenletExit + // into the greenlet. Doing so calls g_switch(), which performs a stack + // switch and runs Python code via _PyEval_EvalFrameDefault. On Python + // < 3.11, executing Python code in a partially-torn-down interpreter + // leads to SIGSEGV (greenlet 3.x) or SIGABRT (greenlet 2.x). + // + // Python 3.11+ restructured interpreter finalization internals (frame + // representation, data stack management, recursion tracking) so that + // g_switch() during finalization is safe. On older Pythons, we simply + // mark the greenlet dead without throwing, which avoids the crash at + // the cost of not running any cleanup code inside the greenlet. + // + // See: https://github.com/python-greenlet/greenlet/issues/411 + // https://github.com/python-greenlet/greenlet/issues/351 + if (greenlet::IsShuttingDown()) { + self->murder_in_place(); + return 1; + } + + /* Hacks hacks hacks copied from instance_dealloc() */ + /* Temporarily resurrect the greenlet. */ + assert(self.REFCNT() == 0); + Py_SET_REFCNT(self.borrow(), 1); + /* Save the current exception, if any. */ + PyErrPieces saved_err; + try { + // BY THE TIME WE GET HERE, the state may actually be going + // away + // if we're shutting down the interpreter and freeing thread + // entries, + // this could result in freeing greenlets that were leaked. So + // we can't try to read the state. + self->deallocing_greenlet_in_thread( + self->thread_state() + ? static_cast(GET_THREAD_STATE()) + : nullptr); + } + catch (const PyErrOccurred&) { + PyErr_WriteUnraisable(self.borrow_o()); + /* XXX what else should we do? */ + } + /* Check for no resurrection must be done while we keep + * our internal reference, otherwise PyFile_WriteObject + * causes recursion if using Py_INCREF/Py_DECREF + */ + if (self.REFCNT() == 1 && self->active()) { + /* Not resurrected, but still not dead! + XXX what else should we do? we complain. */ + PyObject* f = PySys_GetObject("stderr"); + Py_INCREF(self.borrow_o()); /* leak! */ + if (f != NULL) { + // PySys_GetObject returns a borrowed ref which could go + // away when we run arbitrary code, as we do for any of + // the ``PyFile_Write`` APIs. + Py_INCREF(f); + // Note that we're not handling errors here. They either + // work or they don't, and any exception they raised will + // be replaced by PyErrRestore. + PyFile_WriteString("GreenletExit did not kill ", f); + PyFile_WriteObject(self.borrow_o(), f, 0); + PyFile_WriteString("\n", f); + Py_DECREF(f); + } + } + /* Restore the saved exception. */ + saved_err.PyErrRestore(); + /* Undo the temporary resurrection; can't use DECREF here, + * it would cause a recursive call. + */ + assert(self.REFCNT() > 0); + + Py_ssize_t refcnt = self.REFCNT() - 1; + Py_SET_REFCNT(self.borrow_o(), refcnt); + if (refcnt != 0) { + /* Resurrected! */ + _Py_NewReference(self.borrow_o()); + Py_SET_REFCNT(self.borrow_o(), refcnt); + /* Better to use tp_finalizer slot (PEP 442) + * and call ``PyObject_CallFinalizerFromDealloc``, + * but that's only supported in Python 3.4+; see + * Modules/_io/iobase.c for an example. + * TODO: We no longer run on anything that old, switch to finalizers. + * + * The following approach is copied from iobase.c in CPython 2.7. + * (along with much of this function in general). Here's their + * comment: + * + * When called from a heap type's dealloc, the type will be + * decref'ed on return (see e.g. subtype_dealloc in typeobject.c). + * + * On free-threaded builds of CPython, the type is meant to be immortal + * so we probably shouldn't mess with this? See + * test_issue_245_reference_counting_subclass_no_threads + */ + if (PyType_HasFeature(self.TYPE(), Py_TPFLAGS_HEAPTYPE)) { + Py_INCREF(self.TYPE()); + } + + PyObject_GC_Track((PyObject*)self); + + GREENLET_Py_DEC_REFTOTAL; +#ifdef COUNT_ALLOCS + --Py_TYPE(self)->tp_frees; + --Py_TYPE(self)->tp_allocs; +#endif /* COUNT_ALLOCS */ + return 0; + } + return 1; +} + + +static void +green_dealloc(PyGreenlet* self) +{ + PyObject_GC_UnTrack(self); + BorrowedGreenlet me(self); + if (me->active() + && me->started() + && !me->main()) { + if (!_green_dealloc_kill_started_non_main_greenlet(me)) { + return; + } + } + + if (self->weakreflist != NULL) { + PyObject_ClearWeakRefs((PyObject*)self); + } + Py_CLEAR(self->dict); + + if (self->pimpl) { + // In case deleting this, which frees some memory, + // somehow winds up calling back into us. That's usually a + //bug in our code. + Greenlet* p = self->pimpl; + self->pimpl = nullptr; + delete p; + } + // and finally we're done. self is now invalid. + Py_TYPE(self)->tp_free((PyObject*)self); +} + + + +static OwnedObject +internal_green_throw(BorrowedGreenlet self, PyErrPieces& err_pieces) +{ + PyObject* result = nullptr; + err_pieces.PyErrRestore(); + assert(PyErr_Occurred()); + if (self->started() && !self->active()) { + /* dead greenlet: turn GreenletExit into a regular return */ + result = g_handle_exit(OwnedObject()).relinquish_ownership(); + } + self->args() <<= result; + + return single_result(self->g_switch()); +} + + + +PyDoc_STRVAR( + green_switch_doc, + "switch(*args, **kwargs)\n" + "\n" + "Switch execution to this greenlet.\n" + "\n" + "If this greenlet has never been run, then this greenlet\n" + "will be switched to using the body of ``self.run(*args, **kwargs)``.\n" + "\n" + "If the greenlet is active (has been run, but was switch()'ed\n" + "out before leaving its run function), then this greenlet will\n" + "be resumed and the return value to its switch call will be\n" + "None if no arguments are given, the given argument if one\n" + "argument is given, or the args tuple and keyword args dict if\n" + "multiple arguments are given.\n" + "\n" + "If the greenlet is dead, or is the current greenlet then this\n" + "function will simply return the arguments using the same rules as\n" + "above.\n"); + +static PyObject* +green_switch(PyGreenlet* self, PyObject* args, PyObject* kwargs) +{ + // Our use of Greenlet::args() makes this method non-reentrant. + // Therefore, check to be sure the switch will be allowed --- + // we're calling from the same thread that ``self`` belongs to --- + // BEFORE doing anything with args(). If we don't do this, we can + // find args() getting clobbered by switches that will never + // succeed. + // + // TODO: We're only doing this for free-threaded builds because + // those are the only ones that have demonstrated an issue, + // trusting our later checks in g_switch to perform the same + // function and the GIL to keep us from being reentered in regular + // builds. BUT should we always do this as an extra measure of + // safety in case we run code at unexpected times (e.g., a GC?) +#ifdef Py_GIL_DISABLED + try { + self->pimpl->check_switch_allowed(); + } + catch (const PyErrOccurred&) { + return nullptr; + } +#endif + + + using greenlet::SwitchingArgs; + SwitchingArgs switch_args(OwnedObject::owning(args), OwnedObject::owning(kwargs)); + self->pimpl->may_switch_away(); + self->pimpl->args() <<= switch_args; + + // If we're switching out of a greenlet, and that switch is the + // last thing the greenlet does, the greenlet ought to be able to + // go ahead and die at that point. Currently, someone else must + // manually switch back to the greenlet so that we "fall off the + // end" and can perform cleanup. You'd think we'd be able to + // figure out that this is happening using the frame's ``f_lasti`` + // member, which is supposed to be an index into + // ``frame->f_code->co_code``, the bytecode string. However, in + // recent interpreters, ``f_lasti`` tends not to be updated thanks + // to things like the PREDICT() macros in ceval.c. So it doesn't + // really work to do that in many cases. For example, the Python + // code: + // def run(): + // greenlet.getcurrent().parent.switch() + // produces bytecode of len 16, with the actual call to switch() + // being at index 10 (in Python 3.10). However, the reported + // ``f_lasti`` we actually see is...5! (Which happens to be the + // second byte of the CALL_METHOD op for ``getcurrent()``). + + try { + OwnedObject result(single_result(self->pimpl->g_switch())); +#ifndef NDEBUG + // Note that the current greenlet isn't necessarily self. If self + // finished, we went to one of its parents. + assert(!self->pimpl->args()); + + const BorrowedGreenlet& current = GET_THREAD_STATE().state().borrow_current(); + // It's possible it's never been switched to. + assert(!current->args()); +#endif + PyObject* p = result.relinquish_ownership(); + + if (!p && !PyErr_Occurred()) { + // This shouldn't be happening anymore, so the asserts + // are there for debug builds. Non-debug builds + // crash "gracefully" in this case, although there is an + // argument to be made for killing the process in all + // cases --- for this to be the case, our switches + // probably nested in an incorrect way, so the state is + // suspicious. Nothing should be corrupt though, just + // confused at the Python level. Letting this propagate is + // probably good enough. + assert(p || PyErr_Occurred()); + throw PyErrOccurred( + mod_globs->PyExc_GreenletError, + "Greenlet.switch() returned NULL without an exception set." + ); + } + return p; + } + catch(const PyErrOccurred&) { + return nullptr; + } +} + +PyDoc_STRVAR( + green_throw_doc, + "Switches execution to this greenlet, but immediately raises the\n" + "given exception in this greenlet. If no argument is provided, the " + "exception\n" + "defaults to `greenlet.GreenletExit`. The normal exception\n" + "propagation rules apply, as described for `switch`. Note that calling " + "this\n" + "method is almost equivalent to the following::\n" + "\n" + " def raiser():\n" + " raise typ, val, tb\n" + " g_raiser = greenlet(raiser, parent=g)\n" + " g_raiser.switch()\n" + "\n" + "except that this trick does not work for the\n" + "`greenlet.GreenletExit` exception, which would not propagate\n" + "from ``g_raiser`` to ``g``.\n"); + +static PyObject* +green_throw(PyGreenlet* self, PyObject* args) +{ + // See green_switch for why we call this early. +#ifdef Py_GIL_DISABLED + try { + self->pimpl->check_switch_allowed(); + } + catch (const PyErrOccurred&) { + return nullptr; + } +#endif + + PyArgParseParam typ(mod_globs->PyExc_GreenletExit); + PyArgParseParam val; + PyArgParseParam tb; + + if (!PyArg_ParseTuple(args, "|OOO:throw", &typ, &val, &tb)) { + return nullptr; + } + + assert(typ.borrow() || val.borrow()); + + self->pimpl->may_switch_away(); + try { + // Both normalizing the error and the actual throw_greenlet + // could throw PyErrOccurred. + PyErrPieces err_pieces(typ.borrow(), val.borrow(), tb.borrow()); + + return internal_green_throw(self, err_pieces).relinquish_ownership(); + } + catch (const PyErrOccurred&) { + return nullptr; + } +} + +static int +green_bool(PyGreenlet* self) +{ + return self->pimpl->active(); +} + +/** + * CAUTION: Allocates memory, may run GC and arbitrary Python code. + */ +static PyObject* +green_getdict(PyGreenlet* self, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + if (self->dict == NULL) { + self->dict = PyDict_New(); + if (self->dict == NULL) { + return NULL; + } + } + Py_INCREF(self->dict); + return self->dict; +} + +static int +green_setdict(PyGreenlet* self, PyObject* val, void* UNUSED(context)) +{ + if (val == NULL) { + PyErr_SetString(PyExc_TypeError, "__dict__ may not be deleted"); + return -1; + } + if (!PyDict_Check(val)) { + PyErr_SetString(PyExc_TypeError, "__dict__ must be a dictionary"); + return -1; + } + PyCriticalObjectSection cs(self); + PyObject* tmp = self->dict; + Py_INCREF(val); + self->dict = val; + Py_XDECREF(tmp); + return 0; +} + +static bool +_green_not_dead(BorrowedGreenlet self) +{ + // XXX: Where else should we do this? + // Probably on entry to most Python-facing functions? + if (self->was_running_in_dead_thread()) { + self->deactivate_and_free(); + return false; + } + return self->active() || !self->started(); +} + + +static PyObject* +green_getdead(PyGreenlet* self, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + if (_green_not_dead(self)) { + Py_RETURN_FALSE; + } + else { + Py_RETURN_TRUE; + } +} + +static PyObject* +green_get_stack_saved(PyGreenlet* self, void* UNUSED(context)) +{ + return PyLong_FromSsize_t(self->pimpl->stack_saved()); +} + + +static PyObject* +green_getrun(PyGreenlet* self, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + try { + OwnedObject result(BorrowedGreenlet(self)->run()); + return result.relinquish_ownership(); + } + catch(const PyErrOccurred&) { + return nullptr; + } +} + + +static int +green_setrun(PyGreenlet* self, PyObject* nrun, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + try { + BorrowedGreenlet(self)->run(nrun); + return 0; + } + catch(const PyErrOccurred&) { + return -1; + } +} + +static PyObject* +green_getparent(PyGreenlet* self, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + return BorrowedGreenlet(self)->parent().acquire_or_None(); +} + + +static int +green_setparent(PyGreenlet* self, PyObject* nparent, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + try { + BorrowedGreenlet(self)->parent(nparent); + } + catch(const PyErrOccurred&) { + return -1; + } + return 0; +} + + +static PyObject* +green_getcontext(const PyGreenlet* self, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + const Greenlet *const g = self->pimpl; + try { + OwnedObject result(g->context()); + return result.relinquish_ownership(); + } + catch(const PyErrOccurred&) { + return nullptr; + } +} + +static int +green_setcontext(PyGreenlet* self, PyObject* nctx, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + try { + BorrowedGreenlet(self)->context(nctx); + return 0; + } + catch(const PyErrOccurred&) { + return -1; + } +} + + +static PyObject* +green_getframe(PyGreenlet* self, void* UNUSED(context)) +{ + PyCriticalObjectSection cs(self); + const PythonState::OwnedFrame& top_frame = BorrowedGreenlet(self)->top_frame(); + return top_frame.acquire_or_None(); +} + + +static PyObject* +green_getstate(PyGreenlet* self) +{ + PyErr_Format(PyExc_TypeError, + "cannot serialize '%s' object", + Py_TYPE(self)->tp_name); + return nullptr; +} + +static PyObject* +green_repr(PyGreenlet* _self) +{ + BorrowedGreenlet self(_self); + /* + Return a string like + + + The handling of greenlets across threads is not super good. + We mostly use the internal definitions of these terms, but they + generally should make sense to users as well. + */ + PyObject* result; + int never_started = !self->started() && !self->active(); + + const char* const tp_name = Py_TYPE(self)->tp_name; + + if (_green_not_dead(self)) { + /* XXX: The otid= is almost useless because you can't correlate it to + any thread identifier exposed to Python. We could use + PyThreadState_GET()->thread_id, but we'd need to save that in the + greenlet, or save the whole PyThreadState object itself. + + As it stands, its only useful for identifying greenlets from the same thread. + */ + const char* state_in_thread; + if (self->was_running_in_dead_thread()) { + // The thread it was running in is dead! + // This can happen, especially at interpreter shut down. + // It complicates debugging output because it may be + // impossible to access the current thread state at that + // time. Thus, don't access the current thread state. + state_in_thread = " (thread exited)"; + } + else { + state_in_thread = GET_THREAD_STATE().state().is_current(self) + ? " current" + : (self->started() ? " suspended" : ""); + } + result = PyUnicode_FromFormat( + "<%s object at %p (otid=%p)%s%s%s%s>", + tp_name, + self.borrow_o(), + self->thread_state(), + state_in_thread, + self->active() ? " active" : "", + never_started ? " pending" : " started", + self->main() ? " main" : "" + ); + } + else { + result = PyUnicode_FromFormat( + "<%s object at %p (otid=%p) %sdead>", + tp_name, + self.borrow_o(), + self->thread_state(), + self->was_running_in_dead_thread() + ? "(thread exited) " + : "" + ); + } + + return result; +} + + +static PyMethodDef green_methods[] = { + { + .ml_name="switch", + .ml_meth=reinterpret_cast(green_switch), + .ml_flags=METH_VARARGS | METH_KEYWORDS, + .ml_doc=green_switch_doc + }, + {.ml_name="throw", .ml_meth=(PyCFunction)green_throw, .ml_flags=METH_VARARGS, .ml_doc=green_throw_doc}, + {.ml_name="__getstate__", .ml_meth=(PyCFunction)green_getstate, .ml_flags=METH_NOARGS, .ml_doc=NULL}, + {.ml_name=NULL, .ml_meth=NULL} /* sentinel */ +}; + +static PyGetSetDef green_getsets[] = { + /* name, getter, setter, doc, context pointer */ + {.name="__dict__", .get=(getter)green_getdict, .set=(setter)green_setdict}, + {.name="run", .get=(getter)green_getrun, .set=(setter)green_setrun}, + {.name="parent", .get=(getter)green_getparent, .set=(setter)green_setparent}, + {.name="gr_frame", .get=(getter)green_getframe }, + { + .name="gr_context", + .get=(getter)green_getcontext, + .set=(setter)green_setcontext + }, + {.name="dead", .get=(getter)green_getdead}, + {.name="_stack_saved", .get=(getter)green_get_stack_saved}, + {.name=NULL} +}; + +static PyMemberDef green_members[] = { + {.name=NULL} +}; + +static PyNumberMethods green_as_number = { + .nb_bool=(inquiry)green_bool, +}; + + +PyTypeObject PyGreenlet_Type = { + .ob_base=PyVarObject_HEAD_INIT(NULL, 0) + .tp_name="greenlet.greenlet", /* tp_name */ + .tp_basicsize=sizeof(PyGreenlet), /* tp_basicsize */ + /* methods */ + .tp_dealloc=(destructor)green_dealloc, /* tp_dealloc */ + .tp_repr=(reprfunc)green_repr, /* tp_repr */ + .tp_as_number=&green_as_number, /* tp_as _number*/ + .tp_flags=G_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + .tp_doc="greenlet(run=None, parent=None) -> greenlet\n\n" + "Creates a new greenlet object (without running it).\n\n" + " - *run* -- The callable to invoke.\n" + " - *parent* -- The parent greenlet. The default is the current " + "greenlet.", /* tp_doc */ + .tp_traverse=(traverseproc)green_traverse, /* tp_traverse */ + .tp_clear=(inquiry)green_clear, /* tp_clear */ + .tp_weaklistoffset=offsetof(PyGreenlet, weakreflist), /* tp_weaklistoffset */ + + .tp_methods=green_methods, /* tp_methods */ + .tp_members=green_members, /* tp_members */ + .tp_getset=green_getsets, /* tp_getset */ + .tp_dictoffset=offsetof(PyGreenlet, dict), /* tp_dictoffset */ + .tp_init=(initproc)green_init, /* tp_init */ + .tp_alloc=PyType_GenericAlloc, /* tp_alloc */ + .tp_new=(newfunc)green_new, /* tp_new */ + .tp_free=PyObject_GC_Del, /* tp_free */ +#if !GREENLET_PY315 && !(GREENLET_PY314 && defined(Py_GIL_DISABLED)) +/* + We may have been handling this wrong all along. + + It shows as a problem with the GIL disabled. In builds of 3.14 with + assertions enabled, we break the garbage collector if we *ever* + return false from this function. The docs say this is to distinguish + some objects that are collectable vs some that are not, specifically + giving the example of PyTypeObject as the only place this is done, + where it distinguishes between static types like this one (allocated + by the C runtime at load time) and dynamic heap types (created at + runtime as objects). With the GIL disabled, all allocations that are + potentially collectable go in the mimalloc heap, and the collector + asserts that tp_is_gc() is true for them as it walks through the + heap object by object. Since we set the Py_TPFLAGS_HAS_GC bit, we + are always allocated in that mimalloc heap, so we must always be + collectable. + + XXX: TODO: Could this be responsible for some apparent leaks, even + on GIL builds, at least in 3.14? See if we can catch an assertion + failure in the GC on regular 3.14 as well. + */ + .tp_is_gc=(inquiry)green_is_gc, /* tp_is_gc */ +#endif +}; + +#endif + +// Local Variables: +// flycheck-clang-include-path: ("/opt/local/Library/Frameworks/Python.framework/Versions/3.8/include/python3.8") +// End: diff --git a/venv/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp b/venv/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp new file mode 100644 index 0000000..df6cd80 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp @@ -0,0 +1,35 @@ +#ifndef PYGREENLET_HPP +#define PYGREENLET_HPP + + +#include "greenlet.h" +#include "greenlet_compiler_compat.hpp" +#include "greenlet_refs.hpp" + + +using greenlet::refs::OwnedGreenlet; +using greenlet::refs::BorrowedGreenlet; +using greenlet::refs::BorrowedObject;; +using greenlet::refs::OwnedObject; +using greenlet::refs::PyErrPieces; + + +// XXX: These doesn't really belong here, it's not a Python slot. +static OwnedObject internal_green_throw(BorrowedGreenlet self, PyErrPieces& err_pieces); + +static PyGreenlet* green_new(PyTypeObject* type, PyObject* UNUSED(args), PyObject* UNUSED(kwds)); +static int green_clear(PyGreenlet* self); +static int green_init(PyGreenlet* self, PyObject* args, PyObject* kwargs); +static int green_setparent(PyGreenlet* self, PyObject* nparent, void* UNUSED(context)); +static int green_setrun(PyGreenlet* self, PyObject* nrun, void* UNUSED(context)); +static int green_traverse(PyGreenlet* self, visitproc visit, void* arg); +static void green_dealloc(PyGreenlet* self); +static PyObject* green_getparent(PyGreenlet* self, void* UNUSED(context)); + +static int green_is_gc(PyObject* self); +static PyObject* green_getdead(PyGreenlet* self, void* UNUSED(context)); +static PyObject* green_getrun(PyGreenlet* self, void* UNUSED(context)); +static int green_setcontext(PyGreenlet* self, PyObject* nctx, void* UNUSED(context)); +static PyObject* green_getframe(PyGreenlet* self, void* UNUSED(context)); +static PyObject* green_repr(PyGreenlet* self); +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp b/venv/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp new file mode 100644 index 0000000..729cb73 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp @@ -0,0 +1,150 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + Implementation of the Python slots for PyGreenletUnswitchable_Type +*/ +#ifndef PY_GREENLET_UNSWITCHABLE_CPP +#define PY_GREENLET_UNSWITCHABLE_CPP + + + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" // PyMemberDef + +#include "greenlet_internal.hpp" +// Code after this point can assume access to things declared in stdint.h, +// including the fixed-width types. This goes for the platform-specific switch functions +// as well. +#include "greenlet_refs.hpp" +#include "greenlet_slp_switch.hpp" + +#include "greenlet_thread_support.hpp" +#include "TGreenlet.hpp" + +#include "TGreenlet.cpp" +#include "TGreenletGlobals.cpp" +#include "TThreadStateDestroy.cpp" + + +using greenlet::LockGuard; +using greenlet::LockInitError; +using greenlet::PyErrOccurred; +using greenlet::Require; + +using greenlet::g_handle_exit; +using greenlet::single_result; + +using greenlet::Greenlet; +using greenlet::UserGreenlet; +using greenlet::MainGreenlet; +using greenlet::BrokenGreenlet; +using greenlet::ThreadState; +using greenlet::PythonState; + + +#include "PyGreenlet.hpp" + +static PyGreenlet* +green_unswitchable_new(PyTypeObject* type, PyObject* UNUSED(args), PyObject* UNUSED(kwds)) +{ + PyGreenlet* o = + (PyGreenlet*)PyBaseObject_Type.tp_new(type, mod_globs->empty_tuple, mod_globs->empty_dict); + if (o) { + new BrokenGreenlet(o, GET_THREAD_STATE().state().borrow_current()); + assert(Py_REFCNT(o) == 1); + } + return o; +} + +static PyObject* +green_unswitchable_getforce(PyGreenlet* self, void* UNUSED(context)) +{ + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + return PyBool_FromLong(broken->_force_switch_error); +} + +static int +green_unswitchable_setforce(PyGreenlet* self, PyObject* nforce, void* UNUSED(context)) +{ + if (!nforce) { + PyErr_SetString( + PyExc_AttributeError, + "Cannot delete force_switch_error" + ); + return -1; + } + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + int is_true = PyObject_IsTrue(nforce); + if (is_true == -1) { + return -1; + } + broken->_force_switch_error = is_true; + return 0; +} + +static PyObject* +green_unswitchable_getforceslp(PyGreenlet* self, void* UNUSED(context)) +{ + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + return PyBool_FromLong(broken->_force_slp_switch_error); +} + +static int +green_unswitchable_setforceslp(PyGreenlet* self, PyObject* nforce, void* UNUSED(context)) +{ + if (!nforce) { + PyErr_SetString( + PyExc_AttributeError, + "Cannot delete force_slp_switch_error" + ); + return -1; + } + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + int is_true = PyObject_IsTrue(nforce); + if (is_true == -1) { + return -1; + } + broken->_force_slp_switch_error = is_true; + return 0; +} + +static PyGetSetDef green_unswitchable_getsets[] = { + /* name, getter, setter, doc, closure (context pointer) */ + { + .name="force_switch_error", + .get=(getter)green_unswitchable_getforce, + .set=(setter)green_unswitchable_setforce, + .doc=nullptr + }, + { + .name="force_slp_switch_error", + .get=(getter)green_unswitchable_getforceslp, + .set=(setter)green_unswitchable_setforceslp, + .doc=nullptr + }, + {.name=nullptr} +}; + +PyTypeObject PyGreenletUnswitchable_Type = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "greenlet._greenlet.UnswitchableGreenlet", + .tp_dealloc = (destructor)green_dealloc, + .tp_flags = G_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "Undocumented internal class for testing error conditions", + .tp_traverse = (traverseproc)green_traverse, + .tp_clear = (inquiry)green_clear, + + .tp_getset = green_unswitchable_getsets, + .tp_base = &PyGreenlet_Type, + .tp_init = (initproc)green_init, + .tp_alloc = PyType_GenericAlloc, + .tp_new = (newfunc)green_unswitchable_new, + .tp_free = PyObject_GC_Del, +#ifndef Py_GIL_DISABLED + // See comments in the base type + .tp_is_gc = (inquiry)green_is_gc, +#endif +}; + + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/PyModule.cpp b/venv/lib/python3.11/site-packages/greenlet/PyModule.cpp new file mode 100644 index 0000000..f619020 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/PyModule.cpp @@ -0,0 +1,297 @@ +#ifndef PY_MODULE_CPP +#define PY_MODULE_CPP + +#include "greenlet_internal.hpp" + + +#include "TGreenletGlobals.cpp" +#include "TMainGreenlet.cpp" +#include "TThreadStateDestroy.cpp" + +using greenlet::LockGuard; +using greenlet::ThreadState; + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wunused-variable" +#endif + + +PyDoc_STRVAR(mod_getcurrent_doc, + "getcurrent() -> greenlet\n" + "\n" + "Returns the current greenlet (i.e. the one which called this " + "function).\n"); + +static PyObject* +mod_getcurrent(PyObject* UNUSED(module)) +{ + if (greenlet::IsShuttingDown()) { + PyErr_SetString(PyExc_RuntimeError, "greenlet is being finalized"); + return nullptr; + } + return GET_THREAD_STATE().state().get_current().relinquish_ownership_o(); +} + +PyDoc_STRVAR(mod_settrace_doc, + "settrace(callback) -> object\n" + "\n" + "Sets a new tracing function and returns the previous one.\n"); +static PyObject* +mod_settrace(PyObject* UNUSED(module), PyObject* args) +{ + PyArgParseParam tracefunc; + if (!PyArg_ParseTuple(args, "O", &tracefunc)) { + return NULL; + } + ThreadState& state = GET_THREAD_STATE(); + OwnedObject previous = state.get_tracefunc(); + if (!previous) { + previous = Py_None; + } + + state.set_tracefunc(tracefunc); + + return previous.relinquish_ownership(); +} + +PyDoc_STRVAR(mod_gettrace_doc, + "gettrace() -> object\n" + "\n" + "Returns the currently set tracing function, or None.\n"); + +static PyObject* +mod_gettrace(PyObject* UNUSED(module)) +{ + OwnedObject tracefunc = GET_THREAD_STATE().state().get_tracefunc(); + if (!tracefunc) { + tracefunc = Py_None; + } + return tracefunc.relinquish_ownership(); +} + + + +PyDoc_STRVAR(mod_set_thread_local_doc, + "set_thread_local(key, value) -> None\n" + "\n" + "Set a value in the current thread-local dictionary. Debugging only.\n"); + +static PyObject* +mod_set_thread_local(PyObject* UNUSED(module), PyObject* args) +{ + PyArgParseParam key; + PyArgParseParam value; + PyObject* result = NULL; + + if (PyArg_UnpackTuple(args, "set_thread_local", 2, 2, &key, &value)) { + if(PyDict_SetItem( + PyThreadState_GetDict(), // borrow + key, + value) == 0 ) { + // success + Py_INCREF(Py_None); + result = Py_None; + } + } + return result; +} + +PyDoc_STRVAR(mod_get_pending_cleanup_count_doc, + "get_pending_cleanup_count() -> Integer\n" + "\n" + "Get the number of greenlet cleanup operations pending. Testing only.\n"); + + +static PyObject* +mod_get_pending_cleanup_count(PyObject* UNUSED(module)) +{ + LockGuard cleanup_lock(*mod_globs->thread_states_to_destroy_lock); + return PyLong_FromSize_t(mod_globs->thread_states_to_destroy.size()); +} + +PyDoc_STRVAR(mod_get_total_main_greenlets_doc, + "get_total_main_greenlets() -> Integer\n" + "\n" + "Quickly return the number of main greenlets that exist. Testing only.\n"); + +static PyObject* +mod_get_total_main_greenlets(PyObject* UNUSED(module)) +{ + return PyLong_FromSize_t(G_TOTAL_MAIN_GREENLETS); +} + + + +PyDoc_STRVAR(mod_get_clocks_used_doing_optional_cleanup_doc, + "get_clocks_used_doing_optional_cleanup() -> Integer\n" + "\n" + "Get the number of clock ticks the program has used doing optional " + "greenlet cleanup.\n" + "Beginning in greenlet 2.0, greenlet tries to find and dispose of greenlets\n" + "that leaked after a thread exited. This requires invoking Python's garbage collector,\n" + "which may have a performance cost proportional to the number of live objects.\n" + "This function returns the amount of processor time\n" + "greenlet has used to do this. In programs that run with very large amounts of live\n" + "objects, this metric can be used to decide whether the cost of doing this cleanup\n" + "is worth the memory leak being corrected. If not, you can disable the cleanup\n" + "using ``enable_optional_cleanup(False)``.\n" + "The units are arbitrary and can only be compared to themselves (similarly to ``time.clock()``);\n" + "for example, to see how it scales with your heap. You can attempt to convert them into seconds\n" + "by dividing by the value of CLOCKS_PER_SEC." + "If cleanup has been disabled, returns None." + "\n" + "This is an implementation specific, provisional API. It may be changed or removed\n" + "in the future.\n" + ".. versionadded:: 2.0" + ); +static PyObject* +mod_get_clocks_used_doing_optional_cleanup(PyObject* UNUSED(module)) +{ + std::clock_t clocks = ThreadState::clocks_used_doing_gc(); + + if (clocks == std::clock_t(-1)) { + Py_RETURN_NONE; + } + // This might not actually work on some implementations; clock_t + // is an opaque type. + return PyLong_FromSsize_t(clocks); +} + +PyDoc_STRVAR(mod_enable_optional_cleanup_doc, + "mod_enable_optional_cleanup(bool) -> None\n" + "\n" + "Enable or disable optional cleanup operations.\n" + "See ``get_clocks_used_doing_optional_cleanup()`` for details.\n" + ); +static PyObject* +mod_enable_optional_cleanup(PyObject* UNUSED(module), PyObject* flag) +{ + int is_true = PyObject_IsTrue(flag); + if (is_true == -1) { + return nullptr; + } + + if (is_true) { + std::clock_t clocks = ThreadState::clocks_used_doing_gc(); + // If we already have a value, we don't want to lose it. + if (clocks == std::clock_t(-1)) { + ThreadState::set_clocks_used_doing_gc(0); + } + } + else { + ThreadState::set_clocks_used_doing_gc(std::clock_t(-1)); + } + Py_RETURN_NONE; +} + + + + +#if !GREENLET_PY313 +PyDoc_STRVAR(mod_get_tstate_trash_delete_nesting_doc, + "get_tstate_trash_delete_nesting() -> Integer\n" + "\n" + "Return the 'trash can' nesting level. Testing only.\n"); +static PyObject* +mod_get_tstate_trash_delete_nesting(PyObject* UNUSED(module)) +{ + PyThreadState* tstate = PyThreadState_GET(); + +#if GREENLET_PY312 + return PyLong_FromLong(tstate->trash.delete_nesting); +#else + return PyLong_FromLong(tstate->trash_delete_nesting); +#endif +} +#endif + + + + +static PyMethodDef GreenMethods[] = { + { + .ml_name="getcurrent", + .ml_meth=(PyCFunction)mod_getcurrent, + .ml_flags=METH_NOARGS, + .ml_doc=mod_getcurrent_doc + }, + { + .ml_name="settrace", + .ml_meth=(PyCFunction)mod_settrace, + .ml_flags=METH_VARARGS, + .ml_doc=mod_settrace_doc + }, + { + .ml_name="gettrace", + .ml_meth=(PyCFunction)mod_gettrace, + .ml_flags=METH_NOARGS, + .ml_doc=mod_gettrace_doc + }, + { + .ml_name="set_thread_local", + .ml_meth=(PyCFunction)mod_set_thread_local, + .ml_flags=METH_VARARGS, + .ml_doc=mod_set_thread_local_doc + }, + { + .ml_name="get_pending_cleanup_count", + .ml_meth=(PyCFunction)mod_get_pending_cleanup_count, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_pending_cleanup_count_doc + }, + { + .ml_name="get_total_main_greenlets", + .ml_meth=(PyCFunction)mod_get_total_main_greenlets, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_total_main_greenlets_doc + }, + { + .ml_name="get_clocks_used_doing_optional_cleanup", + .ml_meth=(PyCFunction)mod_get_clocks_used_doing_optional_cleanup, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_clocks_used_doing_optional_cleanup_doc + }, + { + .ml_name="enable_optional_cleanup", + .ml_meth=(PyCFunction)mod_enable_optional_cleanup, + .ml_flags=METH_O, + .ml_doc=mod_enable_optional_cleanup_doc + }, +#if !GREENLET_PY313 + { + .ml_name="get_tstate_trash_delete_nesting", + .ml_meth=(PyCFunction)mod_get_tstate_trash_delete_nesting, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_tstate_trash_delete_nesting_doc + }, +#endif + {.ml_name=NULL, .ml_meth=NULL} /* Sentinel */ +}; + +static const char* const copy_on_greentype[] = { + "getcurrent", + "error", + "GreenletExit", + "settrace", + "gettrace", + NULL +}; + +static struct PyModuleDef greenlet_module_def = { + .m_base=PyModuleDef_HEAD_INIT, + .m_name="greenlet._greenlet", + .m_doc=NULL, + .m_size=-1, + .m_methods=GreenMethods, +}; + + +#endif + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp b/venv/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp new file mode 100644 index 0000000..7e9ab5b --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::UserGreenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ + +#include "TGreenlet.hpp" + +namespace greenlet { + +void* BrokenGreenlet::operator new(size_t UNUSED(count)) +{ + return allocator.allocate(1); +} + + +void BrokenGreenlet::operator delete(void* ptr) +{ + return allocator.deallocate(static_cast(ptr), + 1); +} + +greenlet::PythonAllocator greenlet::BrokenGreenlet::allocator; + +bool +BrokenGreenlet::force_slp_switch_error() const noexcept +{ + return this->_force_slp_switch_error; +} + +UserGreenlet::switchstack_result_t BrokenGreenlet::g_switchstack(void) +{ + if (this->_force_switch_error) { + return switchstack_result_t(-1); + } + return UserGreenlet::g_switchstack(); +} + +}; //namespace greenlet diff --git a/venv/lib/python3.11/site-packages/greenlet/TExceptionState.cpp b/venv/lib/python3.11/site-packages/greenlet/TExceptionState.cpp new file mode 100644 index 0000000..08a94ae --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TExceptionState.cpp @@ -0,0 +1,62 @@ +#ifndef GREENLET_EXCEPTION_STATE_CPP +#define GREENLET_EXCEPTION_STATE_CPP + +#include +#include "TGreenlet.hpp" + +namespace greenlet { + + +ExceptionState::ExceptionState() +{ + this->clear(); +} + +void ExceptionState::operator<<(const PyThreadState *const tstate) noexcept +{ + this->exc_info = tstate->exc_info; + this->exc_state = tstate->exc_state; +} + +void ExceptionState::operator>>(PyThreadState *const tstate) noexcept +{ + tstate->exc_state = this->exc_state; + tstate->exc_info = + this->exc_info ? this->exc_info : &tstate->exc_state; + this->clear(); +} + +void ExceptionState::clear() noexcept +{ + this->exc_info = nullptr; + this->exc_state.exc_value = nullptr; +#if !GREENLET_PY311 + this->exc_state.exc_type = nullptr; + this->exc_state.exc_traceback = nullptr; +#endif + this->exc_state.previous_item = nullptr; +} + +int ExceptionState::tp_traverse(visitproc visit, void* arg) noexcept +{ + Py_VISIT(this->exc_state.exc_value); +#if !GREENLET_PY311 + Py_VISIT(this->exc_state.exc_type); + Py_VISIT(this->exc_state.exc_traceback); +#endif + return 0; +} + +void ExceptionState::tp_clear() noexcept +{ + Py_CLEAR(this->exc_state.exc_value); +#if !GREENLET_PY311 + Py_CLEAR(this->exc_state.exc_type); + Py_CLEAR(this->exc_state.exc_traceback); +#endif +} + + +}; // namespace greenlet + +#endif // GREENLET_EXCEPTION_STATE_CPP diff --git a/venv/lib/python3.11/site-packages/greenlet/TGreenlet.cpp b/venv/lib/python3.11/site-packages/greenlet/TGreenlet.cpp new file mode 100644 index 0000000..dcbdc00 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TGreenlet.cpp @@ -0,0 +1,733 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::Greenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef TGREENLET_CPP +#define TGREENLET_CPP +#include "greenlet_internal.hpp" +#include "TGreenlet.hpp" + + +#include "TGreenletGlobals.cpp" +#include "TThreadStateDestroy.cpp" + +namespace greenlet { + +Greenlet::Greenlet(PyGreenlet* p) + : Greenlet(p, StackState()) +{ +} + +Greenlet::Greenlet(PyGreenlet* p, const StackState& initial_stack) + : _self(p), stack_state(initial_stack) +{ + assert(p->pimpl == nullptr); + p->pimpl = this; +} + +Greenlet::~Greenlet() +{ + // XXX: Can't do this. tp_clear is a virtual function, and by the + // time we're here, we've sliced off our child classes. + //this->tp_clear(); + this->_self->pimpl = nullptr; +} + +bool +Greenlet::force_slp_switch_error() const noexcept +{ + return false; +} + +void +Greenlet::release_args() +{ + this->switch_args.CLEAR(); +} + +/** + * CAUTION: This will allocate memory and may trigger garbage + * collection and arbitrary Python code. + */ +OwnedObject +Greenlet::throw_GreenletExit_during_dealloc(const ThreadState& UNUSED(current_thread_state)) +{ + // If we're killed because we lost all references in the + // middle of a switch, that's ok. Don't reset the args/kwargs, + // we still want to pass them to the parent. + PyErr_SetString(mod_globs->PyExc_GreenletExit, + "Killing the greenlet because all references have vanished."); + // To get here it had to have run before + return this->g_switch(); +} + +inline void +Greenlet::slp_restore_state() noexcept +{ +#ifdef SLP_BEFORE_RESTORE_STATE + SLP_BEFORE_RESTORE_STATE(); +#endif + this->stack_state.copy_heap_to_stack( + this->thread_state()->borrow_current()->stack_state); +} + + +inline int +Greenlet::slp_save_state(char *const stackref) noexcept +{ + // XXX: This used to happen in the middle, before saving, but + // after finding the next owner. Does that matter? This is + // only defined for Sparc/GCC where it flushes register + // windows to the stack (I think) +#ifdef SLP_BEFORE_SAVE_STATE + SLP_BEFORE_SAVE_STATE(); +#endif + return this->stack_state.copy_stack_to_heap(stackref, + this->thread_state()->borrow_current()->stack_state); +} + +/** + * CAUTION: This will allocate memory and may trigger garbage + * collection and arbitrary Python code. + */ +OwnedObject +Greenlet::on_switchstack_or_initialstub_failure( + Greenlet* target, + const Greenlet::switchstack_result_t& err, + const bool target_was_me, + const bool was_initial_stub) +{ + // If we get here, either g_initialstub() + // failed, or g_switchstack() failed. Either one of those + // cases SHOULD leave us in the original greenlet with a valid stack. + if (!PyErr_Occurred()) { + PyErr_SetString( + PyExc_SystemError, + was_initial_stub + ? "Failed to switch stacks into a greenlet for the first time." + : "Failed to switch stacks into a running greenlet."); + } + this->release_args(); + + if (target && !target_was_me) { + target->murder_in_place(); + } + + assert(!err.the_new_current_greenlet); + assert(!err.origin_greenlet); + return OwnedObject(); + +} + +OwnedGreenlet +Greenlet::g_switchstack_success() noexcept +{ + PyThreadState* tstate = PyThreadState_GET(); + // restore the saved state + this->python_state >> tstate; + this->exception_state >> tstate; + + // The thread state hasn't been changed yet. + ThreadState* thread_state = this->thread_state(); + OwnedGreenlet result(thread_state->get_current()); + thread_state->set_current(this->self()); + //assert(thread_state->borrow_current().borrow() == this->_self); + return result; +} + +Greenlet::switchstack_result_t +Greenlet::g_switchstack(void) +{ + // if any of these assertions fail, it's likely because we + // switched away and tried to switch back to us. Early stages of + // switching are not reentrant because we re-use ``this->args()``. + // Switching away would happen if we trigger a garbage collection + // (by just using some Python APIs that happen to allocate Python + // objects) and some garbage had weakref callbacks or __del__ that + // switches (people don't write code like that by hand, but with + // gevent it's possible without realizing it) + assert(this->args() || PyErr_Occurred()); + { /* save state */ + if (this->thread_state()->is_current(this->self())) { + // Hmm, nothing to do. + // TODO: Does this bypass trace events that are + // important? + return switchstack_result_t(0, + this, this->thread_state()->borrow_current()); + } + BorrowedGreenlet current = this->thread_state()->borrow_current(); + PyThreadState* tstate = PyThreadState_GET(); + + current->python_state << tstate; + current->exception_state << tstate; + this->python_state.will_switch_from(tstate); + switching_thread_state = this; + current->expose_frames(); + } + assert(this->args() || PyErr_Occurred()); + // If this is the first switch into a greenlet, this will + // return twice, once with 1 in the new greenlet, once with 0 + // in the origin. + int err; + if (this->force_slp_switch_error()) { + err = -1; + } + else { + err = slp_switch(); + } + + if (err < 0) { /* error */ + // Tested by + // test_greenlet.TestBrokenGreenlets.test_failed_to_slp_switch_into_running + // + // It's not clear if it's worth trying to clean up and + // continue here. Failing to switch stacks is a big deal which + // may not be recoverable (who knows what state the stack is in). + // Also, we've stolen references in preparation for calling + // ``g_switchstack_success()`` and we don't have a clean + // mechanism for backing that all out. + Py_FatalError("greenlet: Failed low-level slp_switch(). The stack is probably corrupt."); + } + + // No stack-based variables are valid anymore. + + // But the global is thread_local volatile so we can reload it without the + // compiler caching it from earlier. + Greenlet* greenlet_that_switched_in = switching_thread_state; // aka this + switching_thread_state = nullptr; + // except that no stack variables are valid, we would: + // assert(this == greenlet_that_switched_in); + + // switchstack success is where we restore the exception state, + // etc. It returns the origin greenlet because its convenient. + + OwnedGreenlet origin = greenlet_that_switched_in->g_switchstack_success(); + assert(greenlet_that_switched_in->args() || PyErr_Occurred()); + return switchstack_result_t(err, greenlet_that_switched_in, origin); +} + + +inline void +Greenlet::check_switch_allowed() const +{ + // TODO: Make this take a parameter of the current greenlet, + // or current main greenlet, to make the check for + // cross-thread switching cheaper. Surely somewhere up the + // call stack we've already accessed the thread local variable. + + // We expect to always have a main greenlet now; accessing the thread state + // created it. However, if we get here and cleanup has already + // begun because we're a greenlet that was running in a + // (now dead) thread, these invariants will not hold true. In + // fact, accessing `this->thread_state` may not even be possible. + + // If the thread this greenlet was running in is dead, + // we'll still have a reference to a main greenlet, but the + // thread state pointer we have is bogus (should be nullptr) + // TODO: Give the objects an API to determine if they belong + // to a dead thread. + + const BorrowedMainGreenlet my_main_greenlet = this->find_main_greenlet_in_lineage(); + + if (!my_main_greenlet) { + throw PyErrOccurred(mod_globs->PyExc_GreenletError, + "cannot switch to a garbage collected greenlet"); + } + + if (!my_main_greenlet->thread_state()) { + throw PyErrOccurred(mod_globs->PyExc_GreenletError, + "cannot switch to a different thread (which happens to have exited)"); + } + + // The main greenlet we found was from the .parent lineage. + // That may or may not have any relationship to the main + // greenlet of the running thread. We can't actually access + // our this->thread_state members to try to check that, + // because it could be in the process of getting destroyed, + // but setting the main_greenlet->thread_state member to NULL + // may not be visible yet. So we need to check against the + // current thread state (once the cheaper checks are out of + // the way) + const BorrowedMainGreenlet main_greenlet_cur_thread = GET_THREAD_STATE().state().borrow_main_greenlet(); + if ( + // lineage main greenlet is not this thread's greenlet + main_greenlet_cur_thread != my_main_greenlet + || ( + // atteched to some thread + this->main_greenlet() + // XXX: Same condition as above. Was this supposed to be + // this->main_greenlet()? + && main_greenlet_cur_thread != my_main_greenlet) + // switching into a known dead thread (XXX: which, if we get here, + // is bad, because we just accessed the thread state, which is + // gone!) + || (!main_greenlet_cur_thread->thread_state())) { + // CAUTION: This may trigger memory allocations, gc, and + // arbitrary Python code. + throw PyErrOccurred( + mod_globs->PyExc_GreenletError, + "Cannot switch to a different thread\n\tCurrent: %R\n\tExpected: %R", + main_greenlet_cur_thread, my_main_greenlet); + } +} + +const OwnedObject +Greenlet::context() const +{ + using greenlet::PythonStateContext; + OwnedObject result; + + if (this->is_currently_running_in_some_thread()) { + /* Currently running greenlet: context is stored in the thread state, + not the greenlet object. */ + if (GET_THREAD_STATE().state().is_current(this->self())) { + result = PythonStateContext::context(PyThreadState_GET()); + } + else { + throw ValueError( + "cannot get context of a " + "greenlet that is running in a different thread"); + } + } + else { + /* Greenlet is not running: just return context. */ + result = this->python_state.context(); + } + if (!result) { + result = OwnedObject::None(); + } + return result; +} + + +void +Greenlet::context(BorrowedObject given) +{ + using greenlet::PythonStateContext; + if (!given) { + throw AttributeError("can't delete context attribute"); + } + if (given.is_None()) { + /* "Empty context" is stored as NULL, not None. */ + given = nullptr; + } + + //checks type, incrs refcnt + greenlet::refs::OwnedContext context(given); + PyThreadState* tstate = PyThreadState_GET(); + + if (this->is_currently_running_in_some_thread()) { + if (!GET_THREAD_STATE().state().is_current(this->self())) { + throw ValueError("cannot set context of a greenlet" + " that is running in a different thread"); + } + + /* Currently running greenlet: context is stored in the thread state, + not the greenlet object. */ + OwnedObject octx = OwnedObject::consuming(PythonStateContext::context(tstate)); + PythonStateContext::context(tstate, context.relinquish_ownership()); + } + else { + /* Greenlet is not running: just set context. Note that the + greenlet may be dead.*/ + this->python_state.context() = context; + } +} + +/** + * CAUTION: May invoke arbitrary Python code. + * + * Figure out what the result of ``greenlet.switch(arg, kwargs)`` + * should be and transfers ownership of it to the left-hand-side. + * + * If switch() was just passed an arg tuple, then we'll just return that. + * If only keyword arguments were passed, then we'll pass the keyword + * argument dict. Otherwise, we'll create a tuple of (args, kwargs) and + * return both. + * + * CAUTION: This may allocate a new tuple object, which may + * cause the Python garbage collector to run, which in turn may + * run arbitrary Python code that switches. + */ +OwnedObject& operator<<=(OwnedObject& lhs, greenlet::SwitchingArgs& rhs) noexcept +{ + // Because this may invoke arbitrary Python code, which could + // result in switching back to us, we need to get the + // arguments locally on the stack. + assert(rhs); + OwnedObject args = rhs.args(); + OwnedObject kwargs = rhs.kwargs(); + rhs.CLEAR(); + // We shouldn't be called twice for the same switch. + assert(args || kwargs); + assert(!rhs); + + if (!kwargs) { + lhs = args; + } + else if (!PyDict_Size(kwargs.borrow())) { + lhs = args; + } + else if (!PySequence_Length(args.borrow())) { + lhs = kwargs; + } + else { + // PyTuple_Pack allocates memory, may GC, may run arbitrary + // Python code. + lhs = OwnedObject::consuming(PyTuple_Pack(2, args.borrow(), kwargs.borrow())); + } + return lhs; +} + +static OwnedObject +g_handle_exit(const OwnedObject& greenlet_result) +{ + if (!greenlet_result && mod_globs->PyExc_GreenletExit.PyExceptionMatches()) { + /* catch and ignore GreenletExit */ + PyErrFetchParam val; + // TODO: When we run on 3.12+ only (GREENLET_312), switch to the + // ``PyErr_GetRaisedException`` family of functions. The + // ``PyErr_Fetch`` family is deprecated on 3.12+, but is part + // of the stable ABI so it's not going anywhere. + PyErr_Fetch(PyErrFetchParam(), val, PyErrFetchParam()); + if (!val) { + return OwnedObject::None(); + } + return OwnedObject(val); + } + + if (greenlet_result) { + // package the result into a 1-tuple + // PyTuple_Pack increments the reference of its arguments, + // so we always need to decref the greenlet result; + // the owner will do that. + return OwnedObject::consuming(PyTuple_Pack(1, greenlet_result.borrow())); + } + + return OwnedObject(); +} + + + +/** + * May run arbitrary Python code. + */ +OwnedObject +Greenlet::g_switch_finish(const switchstack_result_t& err) +{ + assert(err.the_new_current_greenlet == this); + + ThreadState& state = *this->thread_state(); + // Because calling the trace function could do arbitrary things, + // including switching away from this greenlet and then maybe + // switching back, we need to capture the arguments now so that + // they don't change. + OwnedObject result; + if (this->args()) { + result <<= this->args(); + } + else { + assert(PyErr_Occurred()); + } + assert(!this->args()); + try { + // Our only caller handles the bad error case + assert(err.status >= 0); + assert(state.borrow_current() == this->self()); + if (OwnedObject tracefunc = state.get_tracefunc()) { + assert(result || PyErr_Occurred()); + g_calltrace(tracefunc, + result ? mod_globs->event_switch : mod_globs->event_throw, + err.origin_greenlet, + this->self()); + } + // The above could have invoked arbitrary Python code, but + // it couldn't switch back to this object and *also* + // throw an exception, so the args won't have changed. + + if (PyErr_Occurred()) { + // We get here if we fell of the end of the run() function + // raising an exception. The switch itself was + // successful, but the function raised. + // valgrind reports that memory allocated here can still + // be reached after a test run. + throw PyErrOccurred::from_current(); + } + return result; + } + catch (const PyErrOccurred&) { + /* Turn switch errors into switch throws */ + /* Turn trace errors into switch throws */ + this->release_args(); + throw; + } +} + +void +Greenlet::g_calltrace(const OwnedObject& tracefunc, + const greenlet::refs::ImmortalEventName& event, + const BorrowedGreenlet& origin, + const BorrowedGreenlet& target) +{ + PyErrPieces saved_exc; + try { + TracingGuard tracing_guard; + // TODO: We have saved the active exception (if any) that's + // about to be raised. In the 'throw' case, we could provide + // the exception to the tracefunction, which seems very helpful. + tracing_guard.CallTraceFunction(tracefunc, event, origin, target); + } + catch (const PyErrOccurred&) { + // In case of exceptions trace function is removed, + // and any existing exception is replaced with the tracing + // exception. + GET_THREAD_STATE().state().set_tracefunc(Py_None); + throw; + } + + saved_exc.PyErrRestore(); + assert( + (event == mod_globs->event_throw && PyErr_Occurred()) + || (event == mod_globs->event_switch && !PyErr_Occurred()) + ); +} + +void +Greenlet::murder_in_place() +{ + if (this->active()) { + assert(!this->is_currently_running_in_some_thread()); + this->deactivate_and_free(); + } +} + +inline void +Greenlet::deactivate_and_free() +{ + if (!this->active()) { + return; + } + // Throw away any saved stack. + this->stack_state = StackState(); + assert(!this->stack_state.active()); + // Throw away any Python references. + // We're holding a borrowed reference to the last + // frame we executed. Since we borrowed it, the + // normal traversal, clear, and dealloc functions + // ignore it, meaning it leaks. (The thread state + // object can't find it to clear it when that's + // deallocated either, because by definition if we + // got an object on this list, it wasn't + // running and the thread state doesn't have + // this frame.) + // So here, we *do* clear it. + this->python_state.tp_clear(true); +} + +bool +Greenlet::belongs_to_thread(const ThreadState* thread_state) const +{ + if (!this->thread_state() // not running anywhere, or thread + // exited + || !thread_state) { // same, or there is no thread state. + return false; + } + return true; +} + + +void +Greenlet::deallocing_greenlet_in_thread(const ThreadState* current_thread_state) +{ + /* Cannot raise an exception to kill the greenlet if + it is not running in the same thread! */ + if (this->belongs_to_thread(current_thread_state)) { + assert(current_thread_state); + // To get here it had to have run before + /* Send the greenlet a GreenletExit exception. */ + + // We don't care about the return value, only whether an + // exception happened. + this->throw_GreenletExit_during_dealloc(*current_thread_state); + return; + } + + // Not the same thread! Temporarily save the greenlet + // into its thread's deleteme list, *if* it exists. + // If that thread has already exited, and processed its pending + // cleanup, we'll never be able to clean everything up: we won't + // be able to raise an exception. + // That's mostly OK! Since we can't add it to a list, our refcount + // won't increase, and we'll go ahead with the DECREFs later. + + ThreadState *const thread_state = this->thread_state(); + if (thread_state) { + thread_state->delete_when_thread_running(this->self()); + } + else { + // The thread is dead, we can't raise an exception. + // We need to make it look non-active, though, so that dealloc + // finishes killing it. + this->deactivate_and_free(); + } + return; +} + + +int +Greenlet::tp_traverse(visitproc visit, void* arg) +{ + + int result; + if ((result = this->exception_state.tp_traverse(visit, arg)) != 0) { + return result; + } + //XXX: This is ugly. But so is handling everything having to do + //with the top frame. + bool visit_top_frame = this->was_running_in_dead_thread(); + // When true, the thread is dead. Our implicit weak reference to the + // frame is now all that's left; we consider ourselves to + // strongly own it now. + if ((result = this->python_state.tp_traverse(visit, arg, visit_top_frame)) != 0) { + return result; + } + return 0; +} + +int +Greenlet::tp_clear() +{ + bool own_top_frame = this->was_running_in_dead_thread(); + this->exception_state.tp_clear(); + this->python_state.tp_clear(own_top_frame); + if (own_top_frame) { + // Throw away any saved stack state since the owned frame is cleared. + this->stack_state.set_inactive(); + } + return 0; +} + +bool Greenlet::is_currently_running_in_some_thread() const +{ + return this->stack_state.active() && !this->python_state.top_frame(); +} + +#if GREENLET_PY312 +void GREENLET_NOINLINE(Greenlet::expose_frames)() +{ + if (!this->python_state.top_frame()) { + return; + } + + _PyInterpreterFrame* last_complete_iframe = nullptr; + _PyInterpreterFrame* iframe = this->python_state.top_frame()->f_frame; + while (iframe) { + // We must make a copy before looking at the iframe contents, + // since iframe might point to a portion of the greenlet's C stack + // that was spilled when switching greenlets. + _PyInterpreterFrame iframe_copy; + this->stack_state.copy_from_stack(&iframe_copy, iframe, sizeof(*iframe)); + if (!_PyFrame_IsIncomplete(&iframe_copy)) { + // If the iframe were OWNED_BY_CSTACK then it would always be + // incomplete. Since it's not incomplete, it's not on the C stack + // and we can access it through the original `iframe` pointer + // directly. This is important since GetFrameObject might + // lazily _create_ the frame object and we don't want the + // interpreter to lose track of it. + // + #if !GREENLET_PY315 + // This enum value was removed in + // https://github.com/python/cpython/pull/141108 + + assert(iframe_copy.owner != FRAME_OWNED_BY_CSTACK); + #endif + + // We really want to just write: + // PyFrameObject* frame = _PyFrame_GetFrameObject(iframe); + // but _PyFrame_GetFrameObject calls _PyFrame_MakeAndSetFrameObject + // which is not a visible symbol in libpython. The easiest + // way to get a public function to call it is using + // PyFrame_GetBack, which is defined as follows: + // assert(frame != NULL); + // assert(!_PyFrame_IsIncomplete(frame->f_frame)); + // PyFrameObject *back = frame->f_back; + // if (back == NULL) { + // _PyInterpreterFrame *prev = frame->f_frame->previous; + // prev = _PyFrame_GetFirstComplete(prev); + // if (prev) { + // back = _PyFrame_GetFrameObject(prev); + // } + // } + // return (PyFrameObject*)Py_XNewRef(back); + if (!iframe->frame_obj) { + PyFrameObject dummy_frame; + _PyInterpreterFrame dummy_iframe; + dummy_frame.f_back = nullptr; + dummy_frame.f_frame = &dummy_iframe; + // force the iframe to be considered complete without + // needing to check its code object: + dummy_iframe.owner = FRAME_OWNED_BY_GENERATOR; + dummy_iframe.previous = iframe; + assert(!_PyFrame_IsIncomplete(&dummy_iframe)); + // Drop the returned reference immediately; the iframe + // continues to hold a strong reference + Py_XDECREF(PyFrame_GetBack(&dummy_frame)); + assert(iframe->frame_obj); + } + + // This is a complete frame, so make the last one of those we saw + // point at it, bypassing any incomplete frames (which may have + // been on the C stack) in between the two. We're overwriting + // last_complete_iframe->previous and need that to be reversible, + // so we store the original previous ptr in the frame object + // (which we must have created on a previous iteration through + // this loop). The frame object has a bunch of storage that is + // only used when its iframe is OWNED_BY_FRAME_OBJECT, which only + // occurs when the frame object outlives the frame's execution, + // which can't have happened yet because the frame is currently + // executing as far as the interpreter is concerned. So, we can + // reuse it for our own purposes. + assert(iframe->owner == FRAME_OWNED_BY_THREAD + || iframe->owner == FRAME_OWNED_BY_GENERATOR); + if (last_complete_iframe) { + assert(last_complete_iframe->frame_obj); + memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0], + &last_complete_iframe->previous, sizeof(void *)); + last_complete_iframe->previous = iframe; + } + last_complete_iframe = iframe; + } + // Frames that are OWNED_BY_FRAME_OBJECT are linked via the + // frame's f_back while all others are linked via the iframe's + // previous ptr. Since all the frames we traverse are running + // as far as the interpreter is concerned, we don't have to + // worry about the OWNED_BY_FRAME_OBJECT case. + iframe = iframe_copy.previous; + } + + // Give the outermost complete iframe a null previous pointer to + // account for any potential incomplete/C-stack iframes between it + // and the actual top-of-stack + if (last_complete_iframe) { + assert(last_complete_iframe->frame_obj); + memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0], + &last_complete_iframe->previous, sizeof(void *)); + last_complete_iframe->previous = nullptr; + } +} +#else +void Greenlet::expose_frames() +{ + +} +#endif + +}; // namespace greenlet +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/TGreenlet.hpp b/venv/lib/python3.11/site-packages/greenlet/TGreenlet.hpp new file mode 100644 index 0000000..d726a61 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TGreenlet.hpp @@ -0,0 +1,872 @@ +#ifndef GREENLET_GREENLET_HPP +#define GREENLET_GREENLET_HPP +/* + * Declarations of the core data structures. +*/ + +#define PY_SSIZE_T_CLEAN +#include + +#include +#include + +#include "greenlet_compiler_compat.hpp" +#include "greenlet_refs.hpp" +#include "greenlet_cpython_compat.hpp" +#include "greenlet_allocator.hpp" + +using greenlet::refs::OwnedObject; +using greenlet::refs::OwnedGreenlet; +using greenlet::refs::OwnedMainGreenlet; +using greenlet::refs::BorrowedGreenlet; + +#if PY_VERSION_HEX < 0x30B00A6 + // prior to 3.11.0a6 +# define _PyCFrame CFrame +# define _PyInterpreterFrame _interpreter_frame +#endif + +#if GREENLET_PY312 +# define Py_BUILD_CORE +# include "internal/pycore_frame.h" +#endif + +#if GREENLET_PY314 +# include "internal/pycore_interpframe_structs.h" +#if defined(_MSC_VER) || defined(__MINGW64__) +# include "greenlet_msvc_compat.hpp" +#else +# include "internal/pycore_interpframe.h" +#endif +#ifdef Py_GIL_DISABLED +# include "internal/pycore_tstate.h" +# include "internal/pycore_critical_section.h" +#endif +#endif + + +#if GREENLET_PY315 +#include "internal/pycore_gc.h" +#if !defined(_MSC_VER) && !defined(__MINGW64__) +#include "internal/pycore_stackref.h" +#endif +#endif + +// XXX: TODO: Work to remove all virtual functions +// for speed of calling and size of objects (no vtable). +// One pattern is the Curiously Recurring Template +namespace greenlet +{ + class ExceptionState + { + private: + G_NO_COPIES_OF_CLS(ExceptionState); + + // Even though these are borrowed objects, we actually own + // them, when they're not null. + // XXX: Express that in the API. + private: + _PyErr_StackItem* exc_info; + _PyErr_StackItem exc_state; + public: + ExceptionState(); + void operator<<(const PyThreadState *const tstate) noexcept; + void operator>>(PyThreadState* tstate) noexcept; + void clear() noexcept; + + int tp_traverse(visitproc visit, void* arg) noexcept; + void tp_clear() noexcept; + }; + + template + void operator<<(const PyThreadState *const tstate, T& exc); + + class PythonStateContext + { + protected: + greenlet::refs::OwnedContext _context; + public: + inline const greenlet::refs::OwnedContext& context() const + { + return this->_context; + } + inline greenlet::refs::OwnedContext& context() + { + return this->_context; + } + + inline void tp_clear() + { + this->_context.CLEAR(); + } + + template + inline static PyObject* context(T* tstate) + { + return tstate->context; + } + + template + inline static void context(T* tstate, PyObject* new_context) + { + tstate->context = new_context; + tstate->context_ver++; + } + }; + class SwitchingArgs; + class PythonState : public PythonStateContext + { + public: + typedef greenlet::refs::OwnedReference OwnedFrame; + private: + G_NO_COPIES_OF_CLS(PythonState); + // We own this if we're suspended (although currently we don't + // tp_traverse into it; that's a TODO). If we're running, it's + // empty. If we get deallocated and *still* have a frame, it + // won't be reachable from the place that normally decref's + // it, so we need to do it (hence owning it). + OwnedFrame _top_frame; +#if GREENLET_USE_CFRAME + _PyCFrame* cframe; + int use_tracing; +#endif +#if GREENLET_PY314 + int py_recursion_depth; + // I think this is only used by the JIT. At least, + // we only got errors not switching it when the JIT was enabled. + // Python/generated_cases.c.h:12469: _PyEval_EvalFrameDefault: + // Assertion `tstate->current_executor == NULL' failed. + // see https://github.com/python-greenlet/greenlet/issues/460 + PyObject* current_executor; + _PyStackRef* stackpointer; + #ifdef Py_GIL_DISABLED + _PyCStackRef* c_stack_refs; + // Strong references to the objects held by the _PyCStackRef nodes on + // our C stack, snapshotted by operator<< when we suspend so tp_traverse + // can keep them alive for the free-threaded GC (capture_c_stack_refs + // explains why we snapshot rather than walk the list). Empty while we + // run. + std::vector c_stack_ref_snapshot; + #endif +#elif GREENLET_PY312 + int py_recursion_depth; + int c_recursion_depth; +#else + int recursion_depth; +#endif +#if GREENLET_PY313 + PyObject* delete_later; + uintptr_t critical_section; +#else + int trash_delete_nesting; +#endif +#if GREENLET_PY311 + _PyInterpreterFrame* current_frame; + _PyStackChunk* datastack_chunk; + PyObject** datastack_top; + PyObject** datastack_limit; +#endif + // The PyInterpreterFrame list on 3.12+ contains some entries that are + // on the C stack, which can't be directly accessed while a greenlet is + // suspended. In order to keep greenlet gr_frame introspection working, + // we adjust stack switching to rewrite the interpreter frame list + // to skip these C-stack frames; we call this "exposing" the greenlet's + // frames because it makes them valid to work with in Python. Then when + // the greenlet is resumed we need to remember to reverse the operation + // we did. The C-stack frames are "entry frames" which are a low-level + // interpreter detail; they're not needed for introspection, but do + // need to be present for the eval loop to work. + void unexpose_frames(); + +#if GREENLET_PY314 && defined(Py_GIL_DISABLED) + // Take a strong reference to every object held by tstate's _PyCStackRef + // list into c_stack_ref_snapshot so tp_traverse can keep them alive + // while we're suspended. Must run while our C stack is still live + // (operator<<). The snapshot is dropped by clear()ing the vector. + void capture_c_stack_refs(const PyThreadState* tstate) noexcept; +#endif + + public: + + PythonState(); + // You can use this for testing whether we have a frame + // or not. It returns const so they can't modify it. + const OwnedFrame& top_frame() const noexcept; + + inline void operator<<(const PyThreadState *const tstate) noexcept; + inline void operator>>(PyThreadState* tstate) noexcept; + void clear() noexcept; + + int tp_traverse(visitproc visit, void* arg, bool visit_top_frame) noexcept; + void tp_clear(bool own_top_frame) noexcept; + void set_initial_state(const PyThreadState* const tstate) noexcept; +#if GREENLET_USE_CFRAME + void set_new_cframe(_PyCFrame& frame) noexcept; +#endif + + void may_switch_away() noexcept; + inline void will_switch_from(PyThreadState *const origin_tstate) noexcept; + void did_finish(PyThreadState* tstate) noexcept; + }; + + class StackState + { + // By having only plain C (POD) members, no virtual functions + // or bases, we get a trivial assignment operator generated + // for us. However, that's not safe since we do manage memory. + // So we declare an assignment operator that only works if we + // don't have any memory allocated. (We don't use + // std::shared_ptr for reference counting just to keep this + // object small) + private: + char* _stack_start; + char* stack_stop; + char* stack_copy; + intptr_t _stack_saved; + StackState* stack_prev; + inline int copy_stack_to_heap_up_to(const char* const stop) noexcept; + inline void free_stack_copy() noexcept; + + public: + /** + * Creates a started, but inactive, state, using *current* + * as the previous. + */ + StackState(void* mark, StackState& current); + /** + * Creates an inactive, unstarted, state. + */ + StackState(); + ~StackState(); + StackState(const StackState& other); + StackState& operator=(const StackState& other); + inline void copy_heap_to_stack(const StackState& current) noexcept; + inline int copy_stack_to_heap(char* const stackref, const StackState& current) noexcept; + inline bool started() const noexcept; + inline bool main() const noexcept; + inline bool active() const noexcept; + inline void set_active() noexcept; + inline void set_inactive() noexcept; + inline intptr_t stack_saved() const noexcept; + inline char* stack_start() const noexcept; + static inline StackState make_main() noexcept; +#ifdef GREENLET_USE_STDIO + friend std::ostream& operator<<(std::ostream& os, const StackState& s); +#endif + + // Fill in [dest, dest + n) with the values that would be at + // [src, src + n) while this greenlet is running. This is like memcpy + // except that if the greenlet is suspended it accounts for the portion + // of the greenlet's stack that was spilled to the heap. `src` may + // be on this greenlet's stack, or on the heap, but not on a different + // greenlet's stack. + void copy_from_stack(void* dest, const void* src, size_t n) const; + }; +#ifdef GREENLET_USE_STDIO + std::ostream& operator<<(std::ostream& os, const StackState& s); +#endif + + class SwitchingArgs + { + private: + G_NO_ASSIGNMENT_OF_CLS(SwitchingArgs); + // If args and kwargs are both false (NULL), this is a *throw*, not a + // switch. PyErr_... must have been called already. + OwnedObject _args; + OwnedObject _kwargs; + public: + + SwitchingArgs() + {} + + SwitchingArgs(const OwnedObject& args, const OwnedObject& kwargs) + : _args(args), + _kwargs(kwargs) + {} + + SwitchingArgs(const SwitchingArgs& other) + : _args(other._args), + _kwargs(other._kwargs) + {} + + const OwnedObject& args() + { + return this->_args; + } + + const OwnedObject& kwargs() + { + return this->_kwargs; + } + + /** + * Moves ownership from the argument to this object. + */ + SwitchingArgs& operator<<=(SwitchingArgs& other) + { + if (this != &other) { + this->_args = other._args; + this->_kwargs = other._kwargs; + other.CLEAR(); + } + return *this; + } + + /** + * Acquires ownership of the argument (consumes the reference). + */ + SwitchingArgs& operator<<=(PyObject* args) + { + this->_args = OwnedObject::consuming(args); + this->_kwargs.CLEAR(); + return *this; + } + + /** + * Acquires ownership of the argument. + * + * Sets the args to be the given value; clears the kwargs. + */ + SwitchingArgs& operator<<=(OwnedObject& args) + { + assert(&args != &this->_args); + this->_args = args; + this->_kwargs.CLEAR(); + args.CLEAR(); + + return *this; + } + + explicit operator bool() const noexcept + { + return this->_args || this->_kwargs; + } + + inline void CLEAR() + { + this->_args.CLEAR(); + this->_kwargs.CLEAR(); + } + + const std::string as_str() const noexcept + { + return PyUnicode_AsUTF8( + OwnedObject::consuming( + PyUnicode_FromFormat( + "SwitchingArgs(args=%R, kwargs=%R)", + this->_args.borrow(), + this->_kwargs.borrow() + ) + ).borrow() + ); + } + }; + + class ThreadState; + + class UserGreenlet; + class MainGreenlet; + + class Greenlet + { + private: + G_NO_COPIES_OF_CLS(Greenlet); + PyGreenlet* const _self; + private: + // XXX: Work to remove these. + friend class ThreadState; + friend class UserGreenlet; + friend class MainGreenlet; + protected: + ExceptionState exception_state; + SwitchingArgs switch_args; + StackState stack_state; + PythonState python_state; + Greenlet(PyGreenlet* p, const StackState& initial_state); + public: + // This constructor takes ownership of the PyGreenlet, by + // setting ``p->pimpl = this;``. + Greenlet(PyGreenlet* p); + virtual ~Greenlet(); + + const OwnedObject context() const; + + // You MUST call this _very_ early in the switching process to + // prepare anything that may need prepared. This might perform + // garbage collections or otherwise run arbitrary Python code. + // + // One specific use of it is for Python 3.11+, preventing + // running arbitrary code at unsafe times. See + // PythonState::may_switch_away(). + inline void may_switch_away() + { + this->python_state.may_switch_away(); + } + + inline void context(refs::BorrowedObject new_context); + + inline SwitchingArgs& args() + { + return this->switch_args; + } + + virtual const refs::BorrowedMainGreenlet main_greenlet() const = 0; + + inline intptr_t stack_saved() const noexcept + { + return this->stack_state.stack_saved(); + } + + // This is used by the macro SLP_SAVE_STATE to compute the + // difference in stack sizes. It might be nice to handle the + // computation ourself, but the type of the result + // varies by platform, so doing it in the macro is the + // simplest way. + inline const char* stack_start() const noexcept + { + return this->stack_state.stack_start(); + } + + virtual OwnedObject throw_GreenletExit_during_dealloc(const ThreadState& current_thread_state); + + /** + * Depends on the state of this->args() or the current Python + * error indicator. Thus, it is not threadsafe or reentrant. + * You (you being ``green_switch``, the Python-level + * ``greenlet.switch`` method) should call + * ``check_switch_allowed`` in free-threaded builds before + * calling this method and catch ``PyErrOccurred`` if it isn't + * a valid switch. This method should also call that method + * because there are places where we can switch internally + * without going through the Python method. + */ + virtual OwnedObject g_switch() = 0; + /** + * Force the greenlet to appear dead. Used when it's not + * possible to throw an exception into a greenlet anymore. + * + * This losses access to the thread state and the main greenlet. + */ + virtual void murder_in_place(); + + /** + * Called when somebody notices we were running in a dead + * thread to allow cleaning up resources (because we can't + * raise GreenletExit into it anymore). + * This is very similar to ``murder_in_place()``, except that + * it DOES NOT lose the main greenlet or thread state. + */ + inline void deactivate_and_free(); + + + // Called when some thread wants to deallocate a greenlet + // object. + // The thread may or may not be the same thread the greenlet + // was running in. + // The thread state will be null if the thread the greenlet + // was running in was known to have exited. + void deallocing_greenlet_in_thread(const ThreadState* current_state); + + // Must be called on 3.12+ before exposing a suspended greenlet's + // frames to user code. This rewrites the linked list of interpreter + // frames to skip the ones that are being stored on the C stack (which + // can't be safely accessed while the greenlet is suspended because + // that stack space might be hosting a different greenlet), and + // sets PythonState::frames_were_exposed so we remember to restore + // the original list before resuming the greenlet. The C-stack frames + // are a low-level interpreter implementation detail; while they're + // important to the bytecode eval loop, they're superfluous for + // introspection purposes. + void expose_frames(); + + + // TODO: Figure out how to make these non-public. + inline void slp_restore_state() noexcept; + inline int slp_save_state(char *const stackref) noexcept; + + inline bool is_currently_running_in_some_thread() const; + virtual bool belongs_to_thread(const ThreadState* state) const; + + inline bool started() const + { + return this->stack_state.started(); + } + inline bool active() const + { + return this->stack_state.active(); + } + inline bool main() const + { + return this->stack_state.main(); + } + virtual refs::BorrowedMainGreenlet find_main_greenlet_in_lineage() const = 0; + + virtual const OwnedGreenlet parent() const = 0; + virtual void parent(const refs::BorrowedObject new_parent) = 0; + + inline const PythonState::OwnedFrame& top_frame() + { + return this->python_state.top_frame(); + } + + virtual const OwnedObject& run() const = 0; + virtual void run(const refs::BorrowedObject nrun) = 0; + + + virtual int tp_traverse(visitproc visit, void* arg); + virtual int tp_clear(); + + + // Return the thread state that the greenlet is running in, or + // null if the greenlet is not running or the thread is known + // to have exited. + virtual ThreadState* thread_state() const noexcept = 0; + + // Return true if the greenlet is known to have been running + // (active) in a thread that has now exited. + virtual bool was_running_in_dead_thread() const noexcept = 0; + + // Return a borrowed greenlet that is the Python object + // this object represents. + inline BorrowedGreenlet self() const noexcept + { + return BorrowedGreenlet(this->_self); + } + + // For testing. If this returns true, we should pretend that + // slp_switch() failed. + virtual bool force_slp_switch_error() const noexcept; + + // Check the preconditions for switching to this greenlet; if they + // aren't met, throws PyErrOccurred. Most callers will want to + // catch this and clear the arguments if they've been set. + inline void check_switch_allowed() const; + + protected: + inline void release_args(); + + // The functions that must not be inlined are declared virtual. + // We also mark them as protected, not private, so that the + // compiler is forced to call them through a function pointer. + // (A sufficiently smart compiler could directly call a private + // virtual function since it can never be overridden in a + // subclass). + + // Also TODO: Switch away from integer error codes and to enums, + // or throw exceptions when possible. + struct switchstack_result_t + { + int status; + Greenlet* the_new_current_greenlet; + OwnedGreenlet origin_greenlet; + + switchstack_result_t() + : status(0), + the_new_current_greenlet(nullptr) + {} + + switchstack_result_t(int err) + : status(err), + the_new_current_greenlet(nullptr) + {} + + switchstack_result_t(int err, Greenlet* state, OwnedGreenlet& origin) + : status(err), + the_new_current_greenlet(state), + origin_greenlet(origin) + { + } + + switchstack_result_t(int err, Greenlet* state, const BorrowedGreenlet& origin) + : status(err), + the_new_current_greenlet(state), + origin_greenlet(origin) + { + } + + switchstack_result_t(const switchstack_result_t& other) + : status(other.status), + the_new_current_greenlet(other.the_new_current_greenlet), + origin_greenlet(other.origin_greenlet) + {} + + switchstack_result_t& operator=(const switchstack_result_t& other) + { + this->status = other.status; + this->the_new_current_greenlet = other.the_new_current_greenlet; + this->origin_greenlet = other.origin_greenlet; + return *this; + } + }; + + OwnedObject on_switchstack_or_initialstub_failure( + Greenlet* target, + const switchstack_result_t& err, + const bool target_was_me=false, + const bool was_initial_stub=false); + + // Returns the previous greenlet we just switched away from. + virtual OwnedGreenlet g_switchstack_success() noexcept; + + class GreenletStartedWhileInPython : public std::runtime_error + { + public: + GreenletStartedWhileInPython() : std::runtime_error("") + {} + }; + + protected: + + + /** + Perform a stack switch into this greenlet. + + This temporarily sets the global variable + ``switching_thread_state`` to this greenlet; as soon as the + call to ``slp_switch`` completes, this is reset to NULL. + Consequently, this depends on the GIL. + + TODO: Adopt the stackman model and pass ``slp_switch`` a + callback function and context pointer; this eliminates the + need for global variables altogether. + + Because the stack switch happens in this function, this + function can't use its own stack (local) variables, set + before the switch, and then accessed after the switch. + + Further, you con't even access ``g_thread_state_global`` + before and after the switch from the global variable. + Because it is thread local some compilers cache it in a + register/on the stack, notably new versions of MSVC; this + breaks with strange crashes sometime later, because writing + to anything in ``g_thread_state_global`` after the switch + is actually writing to random memory. For this reason, we + call a non-inlined function to finish the operation. (XXX: + The ``/GT`` MSVC compiler argument probably fixes that.) + + It is very important that stack switch is 'atomic', i.e. no + calls into other Python code allowed (except very few that + are safe), because global variables are very fragile. (This + should no longer be the case with thread-local variables.) + + */ + // Made virtual to facilitate subclassing UserGreenlet for testing. + virtual switchstack_result_t g_switchstack(void); + +class TracingGuard +{ +private: + PyThreadState* tstate; +public: + TracingGuard() + : tstate(PyThreadState_GET()) + { + PyThreadState_EnterTracing(this->tstate); + } + + ~TracingGuard() + { + PyThreadState_LeaveTracing(this->tstate); + this->tstate = nullptr; + } + + inline void CallTraceFunction(const OwnedObject& tracefunc, + const greenlet::refs::ImmortalEventName& event, + const BorrowedGreenlet& origin, + const BorrowedGreenlet& target) + { + // TODO: This calls tracefunc(event, (origin, target)). Add a shortcut + // function for that that's specialized to avoid the Py_BuildValue + // string parsing, or start with just using "ON" format with PyTuple_Pack(2, + // origin, target). That seems like what the N format is meant + // for. + // XXX: Why does event not automatically cast back to a PyObject? + // It tries to call the "deleted constructor ImmortalEventName + // const" instead. + assert(tracefunc); + assert(event); + assert(origin); + assert(target); + greenlet::refs::NewReference retval( + PyObject_CallFunction( + tracefunc.borrow(), + "O(OO)", + event.borrow(), + origin.borrow(), + target.borrow() + )); + if (!retval) { + throw PyErrOccurred::from_current(); + } + } +}; + + static void + g_calltrace(const OwnedObject& tracefunc, + const greenlet::refs::ImmortalEventName& event, + const greenlet::refs::BorrowedGreenlet& origin, + const BorrowedGreenlet& target); + private: + OwnedObject g_switch_finish(const switchstack_result_t& err); + + }; + + class UserGreenlet : public Greenlet + { + private: + static greenlet::PythonAllocator allocator; + OwnedMainGreenlet _main_greenlet; + OwnedObject _run_callable; + OwnedGreenlet _parent; + public: + static void* operator new(size_t UNUSED(count)); + static void operator delete(void* ptr); + + UserGreenlet(PyGreenlet* p, BorrowedGreenlet the_parent); + virtual ~UserGreenlet(); + + virtual refs::BorrowedMainGreenlet find_main_greenlet_in_lineage() const; + virtual bool was_running_in_dead_thread() const noexcept; + virtual ThreadState* thread_state() const noexcept; + virtual OwnedObject g_switch(); + virtual const OwnedObject& run() const + { + if (this->started() || !this->_run_callable) { + throw AttributeError("run"); + } + return this->_run_callable; + } + virtual void run(const refs::BorrowedObject nrun); + + virtual const OwnedGreenlet parent() const; + virtual void parent(const refs::BorrowedObject new_parent); + + virtual const refs::BorrowedMainGreenlet main_greenlet() const; + + virtual void murder_in_place(); + virtual bool belongs_to_thread(const ThreadState* state) const; + virtual int tp_traverse(visitproc visit, void* arg); + virtual int tp_clear(); + class ParentIsCurrentGuard + { + private: + OwnedGreenlet oldparent; + UserGreenlet* greenlet; + G_NO_COPIES_OF_CLS(ParentIsCurrentGuard); + public: + ParentIsCurrentGuard(UserGreenlet* p, const ThreadState& thread_state); + ~ParentIsCurrentGuard(); + }; + virtual OwnedObject throw_GreenletExit_during_dealloc(const ThreadState& current_thread_state); + protected: + virtual switchstack_result_t g_initialstub(void* mark); + private: + // This function isn't meant to return. + // This accepts raw pointers and the ownership of them at the + // same time. The caller should use ``inner_bootstrap(origin.relinquish_ownership())``. + void inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run); + }; + + class BrokenGreenlet : public UserGreenlet + { + private: + static greenlet::PythonAllocator allocator; + public: + bool _force_switch_error = false; + bool _force_slp_switch_error = false; + + static void* operator new(size_t UNUSED(count)); + static void operator delete(void* ptr); + BrokenGreenlet(PyGreenlet* p, BorrowedGreenlet the_parent) + : UserGreenlet(p, the_parent) + {} + virtual ~BrokenGreenlet() + {} + + virtual switchstack_result_t g_switchstack(void); + virtual bool force_slp_switch_error() const noexcept; + + }; + + class MainGreenlet : public Greenlet + { + private: + static greenlet::PythonAllocator allocator; + refs::BorrowedMainGreenlet _self; + std::atomic _thread_state; + G_NO_COPIES_OF_CLS(MainGreenlet); + public: + static void* operator new(size_t UNUSED(count)); + static void operator delete(void* ptr); + + MainGreenlet(refs::BorrowedMainGreenlet::PyType*, ThreadState*); + virtual ~MainGreenlet(); + + + virtual const OwnedObject& run() const; + virtual void run(const refs::BorrowedObject nrun); + + virtual const OwnedGreenlet parent() const; + virtual void parent(const refs::BorrowedObject new_parent); + + virtual const refs::BorrowedMainGreenlet main_greenlet() const; + + virtual refs::BorrowedMainGreenlet find_main_greenlet_in_lineage() const; + virtual bool was_running_in_dead_thread() const noexcept; + virtual ThreadState* thread_state() const noexcept; + void thread_state(ThreadState*) noexcept; + virtual OwnedObject g_switch(); + virtual int tp_traverse(visitproc visit, void* arg); + }; + + // Instantiate one on the stack to save the GC state, + // and then disable GC. When it goes out of scope, GC will be + // restored to its original state. + class GCDisabledGuard + { + private: + int was_enabled = 0; + public: + GCDisabledGuard() + : was_enabled(PyGC_Disable()) + {} + + ~GCDisabledGuard() + { + if (this->was_enabled) { + PyGC_Enable(); + } + } + }; + + OwnedObject& operator<<=(OwnedObject& lhs, greenlet::SwitchingArgs& rhs) noexcept; + + //TODO: Greenlet::g_switch() should call this automatically on its + //return value. As it is, the module code is calling it. + static inline OwnedObject + single_result(const OwnedObject& results) + { + if (results + && PyTuple_Check(results.borrow()) + && PyTuple_GET_SIZE(results.borrow()) == 1) { + PyObject* result = PyTuple_GET_ITEM(results.borrow(), 0); + assert(result); + return OwnedObject::owning(result); + } + return results; + } + + + static OwnedObject + g_handle_exit(const OwnedObject& greenlet_result); + + + template + void operator<<(const PyThreadState *const lhs, T& rhs) + { + rhs.operator<<(lhs); + } + +} // namespace greenlet ; + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp b/venv/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp new file mode 100644 index 0000000..a67797a --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp @@ -0,0 +1,113 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of GreenletGlobals. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_GREENLET_GLOBALS +#define T_GREENLET_GLOBALS + +#include + +#include "greenlet_refs.hpp" +#include "greenlet_exceptions.hpp" +#include "greenlet_thread_support.hpp" +#include "greenlet_internal.hpp" + +namespace greenlet { + +// This encapsulates what were previously module global "constants" +// established at init time. +// This is a step towards Python3 style module state that allows +// reloading. +// +// In an earlier iteration of this code, we used placement new to be +// able to allocate this object statically still, so that references +// to its members don't incur an extra pointer indirection. +// But under some scenarios, that could result in crashes at +// shutdown because apparently the destructor was getting run twice? +class GreenletGlobals +{ + +public: + const greenlet::refs::ImmortalEventName event_switch; + const greenlet::refs::ImmortalEventName event_throw; + const greenlet::refs::ImmortalException PyExc_GreenletError; + const greenlet::refs::ImmortalException PyExc_GreenletExit; + const greenlet::refs::ImmortalObject empty_tuple; + const greenlet::refs::ImmortalObject empty_dict; + const greenlet::refs::ImmortalString str_run; + Mutex* const thread_states_to_destroy_lock; + greenlet::cleanup_queue_t thread_states_to_destroy; + + GreenletGlobals() : + event_switch("switch"), + event_throw("throw"), + PyExc_GreenletError("greenlet.error"), + PyExc_GreenletExit("greenlet.GreenletExit", PyExc_BaseException), + empty_tuple(Require(PyTuple_New(0))), + empty_dict(Require(PyDict_New())), + str_run("run"), + thread_states_to_destroy_lock(new Mutex()) + {} + + ~GreenletGlobals() + { + // This object is (currently) effectively immortal, and not + // just because of those placement new tricks; if we try to + // deallocate the static object we allocated, and overwrote, + // we would be doing so at C++ teardown time, which is after + // the final Python GIL is released, and we can't use the API + // then. + // (The members will still be destructed, but they also don't + // do any deallocation.) + } + + /** + * Must be holding the ``thread_states_to_destroy`` lock. + */ + void queue_to_destroy(ThreadState* ts) const + { + // we're currently accessed through a static const object, + // implicitly marking our members as const, so code can't just + // call push_back (or pop_back) without casting away the + // const. + // + // Do that for callers. + greenlet::cleanup_queue_t& q = const_cast( + this->thread_states_to_destroy); + // make sure we don't ever try to clean up a state more than + // once. Because they're thread-local, and we ultimately call this + // method from the destructor of the thread local variable, + // we should never find the item already present. This check + // is nominally O(n) in the size of the vector. + assert(std::find(q.begin(), q.end(), ts) == q.end()); + q.push_back(ts); + } + + /** + * Must be holding the ``thread_states_to_destroy`` lock. + */ + ThreadState* take_next_to_destroy() const + { + greenlet::cleanup_queue_t& q = const_cast( + this->thread_states_to_destroy); + if (q.empty()) { + return nullptr; + } + ThreadState* result = q.back(); + q.pop_back(); + return result; + } +}; + +}; // namespace greenlet + +static const greenlet::GreenletGlobals* mod_globs; + +#endif // T_GREENLET_GLOBALS diff --git a/venv/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp b/venv/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp new file mode 100644 index 0000000..e0db0a4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp @@ -0,0 +1,163 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::MainGreenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_MAIN_GREENLET_CPP +#define T_MAIN_GREENLET_CPP + +#include "TGreenlet.hpp" + +#ifdef Py_GIL_DISABLED +#include +#endif + +// Incremented when we create a main greenlet, in a new thread, decremented +// when it is destroyed. +#ifdef Py_GIL_DISABLED +static std::atomic G_TOTAL_MAIN_GREENLETS(0); +#else +// Protected by the GIL. +static Py_ssize_t G_TOTAL_MAIN_GREENLETS; +#endif + +namespace greenlet { +greenlet::PythonAllocator MainGreenlet::allocator; + +void* MainGreenlet::operator new(size_t UNUSED(count)) +{ + return allocator.allocate(1); +} + + +void MainGreenlet::operator delete(void* ptr) +{ + return allocator.deallocate(static_cast(ptr), + 1); +} + + +MainGreenlet::MainGreenlet(PyGreenlet* p, ThreadState* state) + : Greenlet(p, StackState::make_main()), + _self(p), + _thread_state(state) +{ + G_TOTAL_MAIN_GREENLETS++; +} + +MainGreenlet::~MainGreenlet() +{ + G_TOTAL_MAIN_GREENLETS--; + this->tp_clear(); +} + +ThreadState* +MainGreenlet::thread_state() const noexcept +{ + return this->_thread_state; +} + +void +MainGreenlet::thread_state(ThreadState* t) noexcept +{ + // this method is only used during thread tear down, when it is + // called with nullptr, signalling the thread is dead. + assert(!t); + this->_thread_state = t; +} + + +const BorrowedMainGreenlet +MainGreenlet::main_greenlet() const +{ + return this->_self; +} + +BorrowedMainGreenlet +MainGreenlet::find_main_greenlet_in_lineage() const +{ + return BorrowedMainGreenlet(this->_self); +} + +bool +MainGreenlet::was_running_in_dead_thread() const noexcept +{ + return !this->_thread_state; +} + +OwnedObject +MainGreenlet::g_switch() +{ + try { + this->check_switch_allowed(); + } + catch (const PyErrOccurred&) { + this->release_args(); + throw; + } + + switchstack_result_t err = this->g_switchstack(); + if (err.status < 0) { + // XXX: This code path is untested, but it is shared + // with the UserGreenlet path that is tested. + return this->on_switchstack_or_initialstub_failure( + this, + err, + true, // target was me + false // was initial stub + ); + } + + return err.the_new_current_greenlet->g_switch_finish(err); +} + +int +MainGreenlet::tp_traverse(visitproc visit, void* arg) +{ + ThreadState* thread_state = this->_thread_state.load(); + if (thread_state) { + // we've already traversed main, (self), don't do it again. + int result = thread_state->tp_traverse(visit, arg, false); + if (result) { + return result; + } + } + return Greenlet::tp_traverse(visit, arg); +} + +const OwnedObject& +MainGreenlet::run() const +{ + throw AttributeError("Main greenlets do not have a run attribute."); +} + +void +MainGreenlet::run(const BorrowedObject UNUSED(nrun)) +{ + throw AttributeError("Main greenlets do not have a run attribute."); +} + +void +MainGreenlet::parent(const BorrowedObject raw_new_parent) +{ + if (!raw_new_parent) { + throw AttributeError("can't delete attribute"); + } + throw AttributeError("cannot set the parent of a main greenlet"); +} + +const OwnedGreenlet +MainGreenlet::parent() const +{ + return OwnedGreenlet(); // null becomes None +} + +}; // namespace greenlet + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/TPythonState.cpp b/venv/lib/python3.11/site-packages/greenlet/TPythonState.cpp new file mode 100644 index 0000000..bdedebe --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TPythonState.cpp @@ -0,0 +1,614 @@ +#ifndef GREENLET_PYTHON_STATE_CPP +#define GREENLET_PYTHON_STATE_CPP + +#include +#include "TGreenlet.hpp" + +namespace greenlet { + +PythonState::PythonState() + : _top_frame() +#if GREENLET_USE_CFRAME + ,cframe(nullptr) + ,use_tracing(0) +#endif +#if GREENLET_PY314 + ,py_recursion_depth(0) + ,current_executor(nullptr) + ,stackpointer(nullptr) + #ifdef Py_GIL_DISABLED + ,c_stack_refs(nullptr) + #endif +#elif GREENLET_PY312 + ,py_recursion_depth(0) + ,c_recursion_depth(0) +#else + ,recursion_depth(0) +#endif +#if GREENLET_PY313 + ,delete_later(nullptr) + ,critical_section(0) +#else + ,trash_delete_nesting(0) +#endif +#if GREENLET_PY311 + ,current_frame(nullptr) + ,datastack_chunk(nullptr) + ,datastack_top(nullptr) + ,datastack_limit(nullptr) +#endif +{ +#if GREENLET_USE_CFRAME + /* + The PyThreadState->cframe pointer usually points to memory on + the stack, alloceted in a call into PyEval_EvalFrameDefault. + + Initially, before any evaluation begins, it points to the + initial PyThreadState object's ``root_cframe`` object, which is + statically allocated for the lifetime of the thread. + + A greenlet can last for longer than a call to + PyEval_EvalFrameDefault, so we can't set its ``cframe`` pointer + to be the current ``PyThreadState->cframe``; nor could we use + one from the greenlet parent for the same reason. Yet a further + no: we can't allocate one scoped to the greenlet and then + destroy it when the greenlet is deallocated, because inside the + interpreter the _PyCFrame objects form a linked list, and that too + can result in accessing memory beyond its dynamic lifetime (if + the greenlet doesn't actually finish before it dies, its entry + could still be in the list). + + Using the ``root_cframe`` is problematic, though, because its + members are never modified by the interpreter and are set to 0, + meaning that its ``use_tracing`` flag is never updated. We don't + want to modify that value in the ``root_cframe`` ourself: it + *shouldn't* matter much because we should probably never get + back to the point where that's the only cframe on the stack; + even if it did matter, the major consequence of an incorrect + value for ``use_tracing`` is that if its true the interpreter + does some extra work --- however, it's just good code hygiene. + + Our solution: before a greenlet runs, after its initial + creation, it uses the ``root_cframe`` just to have something to + put there. However, once the greenlet is actually switched to + for the first time, ``g_initialstub`` (which doesn't actually + "return" while the greenlet is running) stores a new _PyCFrame on + its local stack, and copies the appropriate values from the + currently running _PyCFrame; this is then made the _PyCFrame for the + newly-minted greenlet. ``g_initialstub`` then proceeds to call + ``glet.run()``, which results in ``PyEval_...`` adding the + _PyCFrame to the list. Switches continue as normal. Finally, when + the greenlet finishes, the call to ``glet.run()`` returns and + the _PyCFrame is taken out of the linked list and the stack value + is now unused and free to expire. + + XXX: I think we can do better. If we're deallocing in the same + thread, can't we traverse the list and unlink our frame? + Can we just keep a reference to the thread state in case we + dealloc in another thread? (Is that even possible if we're still + running and haven't returned from g_initialstub?) + */ + this->cframe = &PyThreadState_GET()->root_cframe; +#endif +} + +#if GREENLET_PY314 && defined(Py_GIL_DISABLED) +void PythonState::capture_c_stack_refs(const PyThreadState* tstate) noexcept +{ + // Runs from operator<< while our C stack is still live and coherent, so we + // can walk tstate's _PyCStackRef list and take a strong reference to every + // object it holds. tp_traverse visits these once we're suspended, because + // by then the nodes themselves have been relocated into the heap stack copy + // and the saved list head no longer points at them. Strong references (not + // _Py_VISIT_STACKREF, whose _PyGC_VisitStackRef isn't exported before 3.15); + // a std::vector rather than a Python list/tuple because operator<< must not + // allocate a GC-tracked object mid-switch. Rebuilt from scratch each time; + // the list is empty at a typical switch, so this is usually just an empty + // loop. + this->c_stack_ref_snapshot.clear(); + for (const _PyCStackRef* node = ((_PyThreadStateImpl*)tstate)->c_stack_refs; + node != nullptr; node = node->next) { + if (!PyStackRef_IsNullOrInt(node->ref)) { + this->c_stack_ref_snapshot.push_back( + OwnedObject::owning(PyStackRef_AsPyObjectBorrow(node->ref))); + } + } +} +#endif + + +inline void PythonState::may_switch_away() noexcept +{ +#if GREENLET_PY311 + // PyThreadState_GetFrame is probably going to have to allocate a + // new frame object. That may trigger garbage collection. Because + // we call this during the early phases of a switch (it doesn't + // matter to which greenlet, as this has a global effect), if a GC + // triggers a switch away, two things can happen, both bad: + // - We might not get switched back to, halting forward progress. + // this is pathological, but possible. + // - We might get switched back to with a different set of + // arguments or a throw instead of a switch. That would corrupt + // our state (specifically, PyErr_Occurred() and this->args() + // would no longer agree). + // + // Thus, when we call this API, we need to have GC disabled. + // This method serves as a bottleneck we call when maybe beginning + // a switch. In this way, it is always safe -- no risk of GC -- to + // use ``_GetFrame()`` whenever we need to, just as it was in + // <=3.10 (because subsequent calls will be cached and not + // allocate memory). + + GCDisabledGuard no_gc; + Py_XDECREF(PyThreadState_GetFrame(PyThreadState_GET())); +#endif +} + +void PythonState::operator<<(const PyThreadState *const tstate) noexcept +{ + this->_context.steal(tstate->context); +#if GREENLET_USE_CFRAME + /* + IMPORTANT: ``cframe`` is a pointer into the STACK. Thus, because + the call to ``slp_switch()`` changes the contents of the stack, + you cannot read from ``ts_current->cframe`` after that call and + necessarily get the same values you get from reading it here. + Anything you need to restore from now to then must be saved in a + global/threadlocal variable (because we can't use stack + variables here either). For things that need to persist across + the switch, use `will_switch_from`. + */ + this->cframe = tstate->cframe; + #if !GREENLET_PY312 + this->use_tracing = tstate->cframe->use_tracing; + #endif +#endif // GREENLET_USE_CFRAME +#if GREENLET_PY311 + #if GREENLET_PY314 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; + this->current_executor = tstate->current_executor; + #ifdef Py_GIL_DISABLED + this->c_stack_refs = ((_PyThreadStateImpl*)tstate)->c_stack_refs; + // Capture the deferred references now, while our C stack is still live, so + // tp_traverse can keep them from being collected while we're suspended. + this->capture_c_stack_refs(tstate); + #endif + #elif GREENLET_PY312 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; + this->c_recursion_depth = Py_C_RECURSION_LIMIT - tstate->c_recursion_remaining; + #else // not 312 + this->recursion_depth = tstate->recursion_limit - tstate->recursion_remaining; + #endif // GREENLET_PY312 + #if GREENLET_PY313 + this->current_frame = tstate->current_frame; + #elif GREENLET_USE_CFRAME + this->current_frame = tstate->cframe->current_frame; + #endif + this->datastack_chunk = tstate->datastack_chunk; + this->datastack_top = tstate->datastack_top; + this->datastack_limit = tstate->datastack_limit; + + PyFrameObject *frame = PyThreadState_GetFrame((PyThreadState *)tstate); + Py_XDECREF(frame); // PyThreadState_GetFrame gives us a new + // reference. + this->_top_frame.steal(frame); + #if GREENLET_PY314 + if (this->top_frame()) { + this->stackpointer = this->_top_frame->f_frame->stackpointer; + } + else { + this->stackpointer = nullptr; + } + #endif + #if GREENLET_PY313 + // By contract of _PyTrash_thread_deposit_object, + // the ``delete_later`` object has a refcount of 0. + // We take a strong reference to it. + // + // Now, ``delete_later`` is managed as a + // linked list whose objects are unconditionally deallocated + // WITHOUT calling DECREF on them, so it's not clear what that is + // actually accomplishing. That is, if another object is pushed on + // the list and then the list is deallocated, this object will + // still be deallocated. This strong reference serves as a form of + // resurrection, meaning that when operator>> DECREFs it, we might + // enter its ``tp_dealloc`` function again. + // + // In practice, it's quite difficult to arrange for this to be + // a non-null value during a greenlet switch. + // ``greenlet.tests.test_greenlet_trash`` tries, but under 3.14, + // at least, fails to do so. + this->delete_later = Py_XNewRef(tstate->delete_later); +#ifdef Py_GIL_DISABLED + // Switching greenlets swaps C stacks, which to the free-threaded runtime is + // the same predicament as detaching the thread: the PyCriticalSection nodes + // chained off tstate->critical_section live on the stack we're leaving, and + // their PyMutexes would stay locked behind our back. The greenlet we switch + // to could then block forever taking one of those same locks -- e.g. an + // asyncio event dispatched onto another fiber re-enters a Task/Future that + // the suspended fiber is mid-step on. So drop the locks here the way + // _PyThreadState_Detach() does and let operator>> re-take them on resume. + if (tstate->critical_section != 0) { + _PyCriticalSection_SuspendAll(const_cast(tstate)); + } +#endif + this->critical_section = tstate->critical_section; + #elif GREENLET_PY312 + this->trash_delete_nesting = tstate->trash.delete_nesting; + #else // not 312 or 3.13+ + this->trash_delete_nesting = tstate->trash_delete_nesting; + #endif // GREENLET_PY312 +#else // Not 311 + this->recursion_depth = tstate->recursion_depth; + this->_top_frame.steal(tstate->frame); + this->trash_delete_nesting = tstate->trash_delete_nesting; +#endif // GREENLET_PY311 +} + +#if GREENLET_PY312 +void GREENLET_NOINLINE(PythonState::unexpose_frames)() +{ + if (!this->top_frame()) { + return; + } + + // See GreenletState::expose_frames() and the comment on frames_were_exposed + // for more information about this logic. + _PyInterpreterFrame *iframe = this->_top_frame->f_frame; + while (iframe != nullptr) { + _PyInterpreterFrame *prev_exposed = iframe->previous; + assert(iframe->frame_obj); + memcpy(&iframe->previous, &iframe->frame_obj->_f_frame_data[0], + sizeof(void *)); + iframe = prev_exposed; + } +} +#else +void PythonState::unexpose_frames() +{} +#endif + +void PythonState::operator>>(PyThreadState *const tstate) noexcept +{ + tstate->context = this->_context.relinquish_ownership(); + /* Incrementing this value invalidates the contextvars cache, + which would otherwise remain valid across switches */ + tstate->context_ver++; +#if GREENLET_USE_CFRAME + tstate->cframe = this->cframe; + /* + If we were tracing, we need to keep tracing. + There should never be the possibility of hitting the + root_cframe here. See note above about why we can't + just copy this from ``origin->cframe->use_tracing``. + */ + #if !GREENLET_PY312 + tstate->cframe->use_tracing = this->use_tracing; + #endif +#endif // GREENLET_USE_CFRAME +#if GREENLET_PY311 + #if GREENLET_PY314 + tstate->py_recursion_remaining = tstate->py_recursion_limit - this->py_recursion_depth; + tstate->current_executor = this->current_executor; + #ifdef Py_GIL_DISABLED + ((_PyThreadStateImpl*)tstate)->c_stack_refs = this->c_stack_refs; + // We're the running greenlet again: our C-stack refs live in the thread + // state now and gc_visit_thread_stacks() covers them, so drop the strong + // references tp_traverse held on our behalf while we were suspended. + this->c_stack_ref_snapshot.clear(); + #endif + this->unexpose_frames(); + #elif GREENLET_PY312 + tstate->py_recursion_remaining = tstate->py_recursion_limit - this->py_recursion_depth; + tstate->c_recursion_remaining = Py_C_RECURSION_LIMIT - this->c_recursion_depth; + this->unexpose_frames(); + #else // \/ 3.11 + tstate->recursion_remaining = tstate->recursion_limit - this->recursion_depth; + #endif // GREENLET_PY312 + #if GREENLET_PY313 + tstate->current_frame = this->current_frame; + #elif GREENLET_USE_CFRAME + tstate->cframe->current_frame = this->current_frame; + #endif + tstate->datastack_chunk = this->datastack_chunk; + tstate->datastack_top = this->datastack_top; + tstate->datastack_limit = this->datastack_limit; +#if GREENLET_PY314 && defined(Py_GIL_DISABLED) + if (this->top_frame()) { + this->_top_frame->f_frame->stackpointer = this->stackpointer; + } +#endif + this->_top_frame.relinquish_ownership(); + #if GREENLET_PY313 + // See comments in operator<<. We own a strong reference to + // this->delete_later, which may or may not be the same object as + // tstate->delete_later (depending if something pushed an object + // onto the trashcan). Again, because ``delete_later`` is managed + // as a linked list, it's not clear that saving and restoring the + // value, especially without ever setting it to NULL, accomplishes + // much...but the code was added by a core dev, so assume correct. + // + // Recall that tstate->delete_later is supposed to have a refcount + // of 0, because objects are added there from their ``tp_dealloc`` + // method. So we should only need to DECREF it if we're the ones + // that INCREF'd it in operator<<. (This is different than the + // core dev's original code which always did this.) + if (this->delete_later == tstate->delete_later) { + Py_XDECREF(tstate->delete_later); + tstate->delete_later = this->delete_later; + this->delete_later = nullptr; + } + else { + // it got switched behind our back. So the reference we own + // needs to be explicitly cleared. + tstate->delete_later = this->delete_later; + Py_CLEAR(this->delete_later); + } + tstate->critical_section = this->critical_section; +#ifdef Py_GIL_DISABLED + // Re-acquire whatever operator<< suspended when this greenlet last yielded. + // A no-op for a greenlet that held no locks, and for a brand-new one whose + // chain starts empty. Mirrors the resume in _PyThreadState_Attach(); note + // _PyCriticalSection_Resume() dereferences the head, so the != 0 guard is + // load-bearing, not just a fast path. + if (tstate->critical_section != 0) { + _PyCriticalSection_Resume(tstate); + } +#endif + + #elif GREENLET_PY312 + tstate->trash.delete_nesting = this->trash_delete_nesting; + #else // not 3.12 + tstate->trash_delete_nesting = this->trash_delete_nesting; + #endif // GREENLET_PY312 +#else // not 3.11 + tstate->frame = this->_top_frame.relinquish_ownership(); + tstate->recursion_depth = this->recursion_depth; + tstate->trash_delete_nesting = this->trash_delete_nesting; +#endif // GREENLET_PY311 +} + +inline void PythonState::will_switch_from(PyThreadState *const origin_tstate) noexcept +{ +#if GREENLET_USE_CFRAME && !GREENLET_PY312 + // The weird thing is, we don't actually save this for an + // effect on the current greenlet, it's saved for an + // effect on the target greenlet. That is, we want + // continuity of this setting across the greenlet switch. + this->use_tracing = origin_tstate->cframe->use_tracing; +#endif +} + +void PythonState::set_initial_state(const PyThreadState* const tstate) noexcept +{ + this->_top_frame = nullptr; +#if GREENLET_PY314 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; + this->current_executor = tstate->current_executor; + #ifdef Py_GIL_DISABLED + // Start with an empty C-stack-ref list, the way a brand-new thread does; + // do NOT copy the parent thread state's head. Those _PyCStackRef nodes sit + // on the parent greenlet's C stack, so once we start running on our own + // stack and overwrite that region, following them reads garbage. The + // free-threaded collector walks c_stack_refs for every thread in + // gc_visit_thread_stacks(), so leaving the stale head here crashed it. + // See https://github.com/python-greenlet/greenlet/issues/515. + this->c_stack_refs = nullptr; + #endif + // this->stackpointer is left null because this->_top_frame is + // null so there is no value to copy. +#elif GREENLET_PY312 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; +#if GREENLET_314 + this->c_recursion_depth = 0; // unused on 3.14 +#else + this->c_recursion_depth = Py_C_RECURSION_LIMIT - tstate->c_recursion_remaining; +#endif +#elif GREENLET_PY311 + this->recursion_depth = tstate->recursion_limit - tstate->recursion_remaining; +#else + this->recursion_depth = tstate->recursion_depth; +#endif +} +// TODO: Better state management about when we own the top frame. +int PythonState::tp_traverse(visitproc visit, void* arg, bool visit_top_frame) noexcept +{ + Py_VISIT(this->_context.borrow()); + if (visit_top_frame) { + Py_VISIT(this->_top_frame.borrow()); + } +#if GREENLET_PY315 + // Visit the references held by our suspended frames. + // This is important specially on free-threading where the + // the suspended frames may contain deferred references to + // objects, and if they are not traversed then the interpreter + // can free objects early causing a use-after-free crash + // at runtime exit. + if (this->_top_frame) { + for (_PyInterpreterFrame* iframe = this->_top_frame->f_frame; + iframe != nullptr; iframe = iframe->previous) { + // Skip generator/coroutine frames; their object's traverse + // already visits them (gen_traverse), so we'd double-count. + // expose_frames leaves them in the ->previous chain. + if (iframe->owner != FRAME_OWNED_BY_THREAD) { + continue; + } + Py_VISIT(iframe->frame_obj); + Py_VISIT(iframe->f_locals); + _Py_VISIT_STACKREF(iframe->f_funcobj); + _Py_VISIT_STACKREF(iframe->f_executable); + int frame_result = _PyGC_VisitFrameStack(iframe, visit, arg); + if (frame_result) { + return frame_result; + } + } + } +#endif +#if GREENLET_PY314 && defined(Py_GIL_DISABLED) + // Visit the objects this greenlet's C-stack refs were holding when it + // suspended (captured by capture_c_stack_refs). The free-threaded collector + // only walks the running thread's _PyCStackRef list in + // gc_visit_thread_stacks(), so without this a collection could free an + // object reachable only through a suspended greenlet's C-stack ref and we'd + // use it after free once the greenlet resumed. The snapshot is empty while + // we're the running greenlet, so this is a no-op there. + for (const OwnedObject& ref : this->c_stack_ref_snapshot) { + Py_VISIT(ref.borrow()); + } +#endif + // Note that we DO NOT visit ``delete_later``. Even if it's + // non-null and we technically own a reference to it, its + // reference count already went to 0 once and it was in the + // process of being deallocated. The trash can mechanism linked it + // into a list that will be cleaned at some later time, and it has + // become untracked by the GC. + return 0; +} + +void PythonState::tp_clear(bool own_top_frame) noexcept +{ + PythonStateContext::tp_clear(); +#if GREENLET_PY314 && defined(Py_GIL_DISABLED) + this->c_stack_ref_snapshot.clear(); +#endif + // If we get here owning a frame, + // we got dealloc'd without being finished. We may or may not be + // in the same thread. + if (own_top_frame) { +#if GREENLET_PY315 + // Release the references held by our suspended frames. + // this->top_frame gets implicitly cleared by the Py_CLEAR(iframe->frame_obj) + // of the first complete frame, so in the end we relinquish ownership of it. + if (this->_top_frame) { + for (_PyInterpreterFrame* iframe = this->_top_frame->f_frame; + iframe != nullptr; iframe = iframe->previous) { + if (iframe->owner != FRAME_OWNED_BY_THREAD) { + continue; + } + // Clear the references held by this frame's evaluation stack. + _PyStackRef* locals = iframe->localsplus; + _PyStackRef* sp = iframe->stackpointer; + if (sp) { + while (sp > locals) { + sp--; + PyStackRef_CLEAR(*sp); + } + iframe->stackpointer = locals; + } + Py_CLEAR(iframe->f_locals); + Py_CLEAR(iframe->frame_obj); + PyStackRef_CLEAR(iframe->f_funcobj); + PyStackRef_CLEAR(iframe->f_executable); + } + } + this->_top_frame.relinquish_ownership(); +#else + this->_top_frame.CLEAR(); +#endif + } +} + +#if GREENLET_USE_CFRAME +void PythonState::set_new_cframe(_PyCFrame& frame) noexcept +{ + frame = *PyThreadState_GET()->cframe; + /* Make the target greenlet refer to the stack value. */ + this->cframe = &frame; + /* + And restore the link to the previous frame so this one gets + unliked appropriately. + */ + this->cframe->previous = &PyThreadState_GET()->root_cframe; +} +#endif + +const PythonState::OwnedFrame& PythonState::top_frame() const noexcept +{ + return this->_top_frame; +} + +void PythonState::did_finish(PyThreadState* tstate) noexcept +{ +#if GREENLET_PY311 + // See https://github.com/gevent/gevent/issues/1924 and + // https://github.com/python-greenlet/greenlet/issues/328. In + // short, Python 3.11 allocates memory for frames as a sort of + // linked list that's kept as part of PyThreadState in the + // ``datastack_chunk`` member and friends. These are saved and + // restored as part of switching greenlets. + // + // When we initially switch to a greenlet, we set those to NULL. + // That causes the frame management code to treat this like a + // brand new thread and start a fresh list of chunks, beginning + // with a new "root" chunk. As we make calls in this greenlet, + // those chunks get added, and as calls return, they get popped. + // But the frame code (pystate.c) is careful to make sure that the + // root chunk never gets popped. + // + // Thus, when a greenlet exits for the last time, there will be at + // least a single root chunk that we must be responsible for + // deallocating. + // + // The complex part is that these chunks are allocated and freed + // using ``_PyObject_VirtualAlloc``/``Free``. Those aren't public + // functions, and they aren't exported for linking. It so happens + // that we know they are just thin wrappers around the Arena + // allocator, so we can use that directly to deallocate in a + // compatible way. + // + // CAUTION: Check this implementation detail on every major version. + // + // It might be nice to be able to do this in our destructor, but + // can we be sure that no one else is using that memory? Plus, as + // described below, our pointers may not even be valid anymore. As + // a special case, there is one time that we know we can do this, + // and that's from the destructor of the associated UserGreenlet + // (NOT main greenlet) + PyObjectArenaAllocator alloc; + _PyStackChunk* chunk = nullptr; + if (tstate) { + // We really did finish, we can never be switched to again. + chunk = tstate->datastack_chunk; + // Unfortunately, we can't do much sanity checking. Our + // this->datastack_chunk pointer is out of date (evaluation may + // have popped down through it already) so we can't verify that + // we deallocate it. I don't think we can even check datastack_top + // for the same reason. + + PyObject_GetArenaAllocator(&alloc); + tstate->datastack_chunk = nullptr; + tstate->datastack_limit = nullptr; + tstate->datastack_top = nullptr; + + } + else if (this->datastack_chunk) { + // The UserGreenlet (NOT the main greenlet!) is being deallocated. If we're + // still holding a stack chunk, it's garbage because we know + // we can never switch back to let cPython clean it up. + // Because the last time we got switched away from, and we + // haven't run since then, we know our chain is valid and can + // be dealloced. + chunk = this->datastack_chunk; + PyObject_GetArenaAllocator(&alloc); + } + + if (alloc.free && chunk) { + // In case the arena mechanism has been torn down already. + while (chunk) { + _PyStackChunk *prev = chunk->previous; + chunk->previous = nullptr; + alloc.free(alloc.ctx, chunk, chunk->size); + chunk = prev; + } + } + + this->datastack_chunk = nullptr; + this->datastack_limit = nullptr; + this->datastack_top = nullptr; +#endif +} + + +}; // namespace greenlet + +#endif // GREENLET_PYTHON_STATE_CPP diff --git a/venv/lib/python3.11/site-packages/greenlet/TStackState.cpp b/venv/lib/python3.11/site-packages/greenlet/TStackState.cpp new file mode 100644 index 0000000..9743ab5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TStackState.cpp @@ -0,0 +1,265 @@ +#ifndef GREENLET_STACK_STATE_CPP +#define GREENLET_STACK_STATE_CPP + +#include "TGreenlet.hpp" + +namespace greenlet { + +#ifdef GREENLET_USE_STDIO +#include +using std::cerr; +using std::endl; + +std::ostream& operator<<(std::ostream& os, const StackState& s) +{ + os << "StackState(stack_start=" << (void*)s._stack_start + << ", stack_stop=" << (void*)s.stack_stop + << ", stack_copy=" << (void*)s.stack_copy + << ", stack_saved=" << s._stack_saved + << ", stack_prev=" << s.stack_prev + << ", addr=" << &s + << ")"; + return os; +} +#endif + +StackState::StackState(void* mark, StackState& current) + : _stack_start(nullptr), + stack_stop((char*)mark), + stack_copy(nullptr), + _stack_saved(0), + /* Skip a dying greenlet */ + stack_prev(current._stack_start + ? ¤t + : current.stack_prev) +{ +} + +StackState::StackState() + : _stack_start(nullptr), + stack_stop(nullptr), + stack_copy(nullptr), + _stack_saved(0), + stack_prev(nullptr) +{ +} + +StackState::StackState(const StackState& other) +// can't use a delegating constructor because of +// MSVC for Python 2.7 + : _stack_start(nullptr), + stack_stop(nullptr), + stack_copy(nullptr), + _stack_saved(0), + stack_prev(nullptr) +{ + this->operator=(other); +} + +StackState& StackState::operator=(const StackState& other) +{ + if (&other == this) { + return *this; + } + if (other._stack_saved) { + throw std::runtime_error("Refusing to steal memory."); + } + + //If we have memory allocated, dispose of it + this->free_stack_copy(); + + this->_stack_start = other._stack_start; + this->stack_stop = other.stack_stop; + this->stack_copy = other.stack_copy; + this->_stack_saved = other._stack_saved; + this->stack_prev = other.stack_prev; + return *this; +} + +inline void StackState::free_stack_copy() noexcept +{ + PyMem_Free(this->stack_copy); + this->stack_copy = nullptr; + this->_stack_saved = 0; +} + +inline void StackState::copy_heap_to_stack(const StackState& current) noexcept +{ + + /* Restore the heap copy back into the C stack */ + if (this->_stack_saved != 0) { + memcpy(this->_stack_start, this->stack_copy, this->_stack_saved); + this->free_stack_copy(); + } + StackState* owner = const_cast(¤t); + if (!owner->_stack_start) { + owner = owner->stack_prev; /* greenlet is dying, skip it */ + } + while (owner && owner->stack_stop <= this->stack_stop) { + // cerr << "\tOwner: " << owner << endl; + owner = owner->stack_prev; /* find greenlet with more stack */ + } + this->stack_prev = owner; + // cerr << "\tFinished with: " << *this << endl; +} + +inline int StackState::copy_stack_to_heap_up_to(const char* const stop) noexcept +{ + /* Save more of g's stack into the heap -- at least up to 'stop' + g->stack_stop |________| + | | + | __ stop . . . . . + | | ==> . . + |________| _______ + | | | | + | | | | + g->stack_start | | |_______| g->stack_copy + */ + intptr_t sz1 = this->_stack_saved; + intptr_t sz2 = stop - this->_stack_start; + assert(this->_stack_start); + if (sz2 > sz1) { + char* c = (char*)PyMem_Realloc(this->stack_copy, sz2); + if (!c) { + PyErr_NoMemory(); + return -1; + } + memcpy(c + sz1, this->_stack_start + sz1, sz2 - sz1); + this->stack_copy = c; + this->_stack_saved = sz2; + } + return 0; +} + +inline int StackState::copy_stack_to_heap(char* const stackref, + const StackState& current) noexcept +{ + /* must free all the C stack up to target_stop */ + const char* const target_stop = this->stack_stop; + + StackState* owner = const_cast(¤t); + assert(owner->_stack_saved == 0); // everything is present on the stack + if (!owner->_stack_start) { + owner = owner->stack_prev; /* not saved if dying */ + } + else { + owner->_stack_start = stackref; + } + + while (owner->stack_stop < target_stop) { + /* ts_current is entierely within the area to free */ + if (owner->copy_stack_to_heap_up_to(owner->stack_stop)) { + return -1; /* XXX */ + } + owner = owner->stack_prev; + } + if (owner != this) { + if (owner->copy_stack_to_heap_up_to(target_stop)) { + return -1; /* XXX */ + } + } + return 0; +} + +inline bool StackState::started() const noexcept +{ + return this->stack_stop != nullptr; +} + +inline bool StackState::main() const noexcept +{ + return this->stack_stop == (char*)-1; +} + +inline bool StackState::active() const noexcept +{ + return this->_stack_start != nullptr; +} + +inline void StackState::set_active() noexcept +{ + assert(this->_stack_start == nullptr); + this->_stack_start = (char*)1; +} + +inline void StackState::set_inactive() noexcept +{ + this->_stack_start = nullptr; + // XXX: What if we still have memory out there? + // That case is actually triggered by + // test_issue251_issue252_explicit_reference_not_collectable (greenlet.tests.test_leaks.TestLeaks) + // and + // test_issue251_issue252_need_to_collect_in_background + // (greenlet.tests.test_leaks.TestLeaks) + // + // Those objects never get deallocated, so the destructor never + // runs. + // It *seems* safe to clean up the memory here? + if (this->_stack_saved) { + this->free_stack_copy(); + } +} + +inline intptr_t StackState::stack_saved() const noexcept +{ + return this->_stack_saved; +} + +inline char* StackState::stack_start() const noexcept +{ + return this->_stack_start; +} + + +inline StackState StackState::make_main() noexcept +{ + StackState s; + s._stack_start = (char*)1; + s.stack_stop = (char*)-1; + return s; +} + +StackState::~StackState() +{ + if (this->_stack_saved != 0) { + this->free_stack_copy(); + } +} + +void StackState::copy_from_stack(void* vdest, const void* vsrc, size_t n) const +{ + char* dest = static_cast(vdest); + const char* src = static_cast(vsrc); + if (src + n <= this->_stack_start + || src >= this->_stack_start + this->_stack_saved + || this->_stack_saved == 0) { + // Nothing we're copying was spilled from the stack + memcpy(dest, src, n); + return; + } + + if (src < this->_stack_start) { + // Copy the part before the saved stack. + // We know src + n > _stack_start due to the test above. + const size_t nbefore = this->_stack_start - src; + memcpy(dest, src, nbefore); + dest += nbefore; + src += nbefore; + n -= nbefore; + } + // We know src >= _stack_start after the before-copy, and + // src < _stack_start + _stack_saved due to the first if condition + size_t nspilled = std::min(n, this->_stack_start + this->_stack_saved - src); + memcpy(dest, this->stack_copy + (src - this->_stack_start), nspilled); + dest += nspilled; + src += nspilled; + n -= nspilled; + if (n > 0) { + // Copy the part after the saved stack + memcpy(dest, src, n); + } +} + +}; // namespace greenlet + +#endif // GREENLET_STACK_STATE_CPP diff --git a/venv/lib/python3.11/site-packages/greenlet/TThreadState.hpp b/venv/lib/python3.11/site-packages/greenlet/TThreadState.hpp new file mode 100644 index 0000000..fe74ce3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TThreadState.hpp @@ -0,0 +1,627 @@ +#ifndef GREENLET_THREAD_STATE_HPP +#define GREENLET_THREAD_STATE_HPP + +#include +#include +#include +#include + +#include "greenlet_internal.hpp" +#include "greenlet_refs.hpp" +#include "greenlet_thread_support.hpp" + +using greenlet::LockGuard; +using greenlet::refs::BorrowedObject; +using greenlet::refs::BorrowedGreenlet; +using greenlet::refs::BorrowedMainGreenlet; +using greenlet::refs::OwnedMainGreenlet; +using greenlet::refs::OwnedObject; +using greenlet::refs::OwnedGreenlet; +using greenlet::refs::OwnedList; +using greenlet::refs::PyErrFetchParam; +using greenlet::refs::PyArgParseParam; +using greenlet::refs::ImmortalString; +using greenlet::refs::CreatedModule; +using greenlet::refs::PyErrPieces; +using greenlet::refs::NewReference; + + +namespace greenlet { +/** + * Thread-local state of greenlets. + * + * Each native thread will get exactly one of these objects, + * automatically accessed through the best available thread-local + * mechanism the compiler supports (``thread_local`` for C++11 + * compilers or ``__thread``/``declspec(thread)`` for older GCC/clang + * or MSVC, respectively.) + * + * Previously, we kept thread-local state mostly in a bunch of + * ``static volatile`` variables in the main greenlet file.. This had + * the problem of requiring extra checks, loops, and great care + * accessing these variables if we potentially invoked any Python code + * that could release the GIL, because the state could change out from + * under us. Making the variables thread-local solves this problem. + * + * When we detected that a greenlet API accessing the current greenlet + * was invoked from a different thread than the greenlet belonged to, + * we stored a reference to the greenlet in the Python thread + * dictionary for the thread the greenlet belonged to. This could lead + * to memory leaks if the thread then exited (because of a reference + * cycle, as greenlets referred to the thread dictionary, and deleting + * non-current greenlets leaked their frame plus perhaps arguments on + * the C stack). If a thread exited while still having running + * greenlet objects (perhaps that had just switched back to the main + * greenlet), and did not invoke one of the greenlet APIs *in that + * thread, immediately before it exited, without some other thread + * then being invoked*, such a leak was guaranteed. + * + * This can be partly solved by using compiler thread-local variables + * instead of the Python thread dictionary, thus avoiding a cycle. + * + * To fully solve this problem, we need a reliable way to know that a + * thread is done and we should clean up the main greenlet. On POSIX, + * we can use the destructor function of ``pthread_key_create``, but + * there's nothing similar on Windows; a C++11 thread local object + * reliably invokes its destructor when the thread it belongs to exits + * (non-C++11 compilers offer ``__thread`` or ``declspec(thread)`` to + * create thread-local variables, but they can't hold C++ objects that + * invoke destructors; the C++11 version is the most portable solution + * I found). When the thread exits, we can drop references and + * otherwise manipulate greenlets and frames that we know can no + * longer be switched to. + * + * There are two small wrinkles. The first is that when the thread + * exits, it is too late to actually invoke Python APIs: the Python + * thread state is gone, and the GIL is released. To solve *this* + * problem, our destructor uses ``Py_AddPendingCall`` to transfer the + * destruction work to the main thread. + * + * The second is that once the thread exits, the thread local object + * is invalid and we can't even access a pointer to it, so we can't + * pass it to ``Py_AddPendingCall``. This is handled by actually using + * a second object that's thread local (ThreadStateCreator) and having + * it dynamically allocate this object so it can live until the + * pending call runs. + */ + + + +class ThreadState { +private: + // As of commit 08ad1dd7012b101db953f492e0021fb08634afad + // this class needed 56 bytes in o Py_DEBUG build + // on 64-bit macOS 11. + // Adding the vector takes us up to 80 bytes () + + /* Strong reference to the main greenlet */ + OwnedMainGreenlet main_greenlet; + + /* Strong reference to the current greenlet. */ + OwnedGreenlet current_greenlet; + + /* Strong reference to the trace function, if any. */ + OwnedObject tracefunc; + + // Use std::allocator (malloc/free) instead of PythonAllocator + // (PyMem_Malloc) for the deleteme list. During Py_FinalizeEx on + // Python < 3.11, the PyObject_Malloc pool that holds ThreadState + // can be disrupted, corrupting any PythonAllocator-backed + // containers. Using std::allocator makes this vector independent + // of Python's allocator lifecycle. + typedef std::vector deleteme_t; + /* A vector of raw PyGreenlet pointers representing things that need + deleted when this thread is running. The vector owns the + references, but you need to manually INCREF/DECREF as you use + them. We don't use a vector because we + make copy of this vector, and that would become O(n) as all the + refcounts are incremented in the copy. + */ + deleteme_t deleteme; +#ifdef Py_GIL_DISABLED + // On free-threaded builds, we need to protect shared access to + // the deleteme list by a mutex. It can be written from one thread + // while being read in another + Mutex deleteme_lock; +#endif + +#ifdef GREENLET_NEEDS_EXCEPTION_STATE_SAVED + void* exception_state; +#endif + +#ifdef Py_GIL_DISABLED + static std::atomic _clocks_used_doing_gc; +#else + static std::clock_t _clocks_used_doing_gc; +#endif + static ImmortalString get_referrers_name; + + G_NO_COPIES_OF_CLS(ThreadState); + + + // Allocates a main greenlet for the thread state. If this fails, + // exits the process. Called only during constructing a ThreadState. + MainGreenlet* alloc_main() + { + PyGreenlet* gmain; + + /* create the main greenlet for this thread */ + gmain = reinterpret_cast(PyType_GenericAlloc(&PyGreenlet_Type, 0)); + if (gmain == NULL) { + throw PyFatalError("alloc_main failed to alloc"); //exits the process + } + + MainGreenlet* const main = new MainGreenlet(gmain, this); + + assert(Py_REFCNT(gmain) == 1); + assert(gmain->pimpl == main); + return main; + } + + +public: + // Allocate ThreadState with malloc/free rather than Python's + // object allocator. ThreadState outlives many Python objects and + // must remain valid throughout Py_FinalizeEx. On Python < 3.11, + // PyObject_Malloc pools can be disrupted during early + // finalization, corrupting any C++ objects stored in them. + static void* operator new(size_t count) + { + void* p = std::malloc(count); + if (!p) { + throw std::bad_alloc(); + } + return p; + } + + static void operator delete(void* ptr) + { + std::free(ptr); + } + + static void init() + { + ThreadState::get_referrers_name = "get_referrers"; + ThreadState::set_clocks_used_doing_gc(0); + } + + ThreadState() + { + +#ifdef GREENLET_NEEDS_EXCEPTION_STATE_SAVED + this->exception_state = slp_get_exception_state(); +#endif + + // XXX: Potentially dangerous, exposing a not fully + // constructed object. + MainGreenlet* const main = this->alloc_main(); + this->main_greenlet = OwnedMainGreenlet::consuming( + main->self() + ); + assert(this->main_greenlet); + this->current_greenlet = main->self(); + // The main greenlet starts with 1 refs: The returned one. We + // then copied it to the current greenlet. + assert(this->main_greenlet.REFCNT() == 2); + } + + inline void restore_exception_state() + { +#ifdef GREENLET_NEEDS_EXCEPTION_STATE_SAVED + // It's probably important this be inlined and only call C + // functions to avoid adding an SEH frame. + slp_set_exception_state(this->exception_state); +#endif + } + + inline bool has_main_greenlet() const noexcept + { + return bool(this->main_greenlet); + } + + // Called from the ThreadStateCreator when we're in non-standard + // threading mode. In that case, there is an object in the Python + // thread state dictionary that points to us. The main greenlet + // also traverses into us, in which case it's crucial not to + // traverse back into the main greenlet. + int tp_traverse(visitproc visit, void* arg, bool traverse_main=true) + { + if (traverse_main) { + Py_VISIT(main_greenlet.borrow_o()); + } + if (traverse_main || current_greenlet != main_greenlet) { + Py_VISIT(current_greenlet.borrow_o()); + } + Py_VISIT(tracefunc.borrow()); + return 0; + } + + inline BorrowedMainGreenlet borrow_main_greenlet() const noexcept + { + assert(this->main_greenlet); + assert(this->main_greenlet.REFCNT() >= 2); + return this->main_greenlet; + }; + + inline OwnedMainGreenlet get_main_greenlet() const noexcept + { + return this->main_greenlet; + } + + /** + * If we have a main greenlet, mark it as dead by setting its + * thread_state to null (this part is atomic with respect to other + * threads looking at the main greenlet's thread_state). + */ + inline bool mark_main_greenlet_dead() noexcept + { + PyGreenlet* main_greenlet = this->main_greenlet.borrow(); + if (!main_greenlet) { + return false; + } + assert(main_greenlet->pimpl->thread_state() == this + || main_greenlet->pimpl->thread_state() == nullptr); + dynamic_cast(main_greenlet->pimpl)->thread_state(nullptr); + return true; + } + + /** + * In addition to returning a new reference to the currunt + * greenlet, this performs any maintenance needed. + */ + inline OwnedGreenlet get_current() + { + /* green_dealloc() cannot delete greenlets from other threads, so + it stores them in the thread dict; delete them now. */ + this->clear_deleteme_list(); + //assert(this->current_greenlet->main_greenlet == this->main_greenlet); + //assert(this->main_greenlet->main_greenlet == this->main_greenlet); + return this->current_greenlet; + } + + /** + * As for non-const get_current(); + */ + inline BorrowedGreenlet borrow_current() + { + this->clear_deleteme_list(); + return this->current_greenlet; + } + + /** + * Does no maintenance. + */ + inline OwnedGreenlet get_current() const + { + return this->current_greenlet; + } + + template + inline bool is_current(const refs::PyObjectPointer& obj) const + { + return this->current_greenlet.borrow_o() == obj.borrow_o(); + } + + inline void set_current(const OwnedGreenlet& target) + { + this->current_greenlet = target; + } + +private: + /** + * Deref and remove the greenlets from the deleteme list. Must be + * holding the GIL. + * + * If *murder* is true, then we must be called from a different + * thread than the one that these greenlets were running in. + * In that case, if the greenlet was actually running, we destroy + * the frame reference and otherwise make it appear dead before + * proceeding; otherwise, we would try (and fail) to raise an + * exception in it and wind up right back in this list. + */ + inline void clear_deleteme_list(const bool murder=false) + { +#ifdef Py_GIL_DISABLED + LockGuard deleteme_guard(this->deleteme_lock); +#endif + if (this->deleteme.empty()) { + return; + } + // Move the list contents out with swap — a constant-time + // pointer exchange that never allocates. The previous + // code used a copy (deleteme_t copy = this->deleteme) + // which allocated through PythonAllocator / PyMem_Malloc; + // that could SIGSEGV during early Py_FinalizeEx on Python + // < 3.11 when the allocator is partially torn down. + deleteme_t copy; + std::swap(copy, this->deleteme); + + // During Py_FinalizeEx cleanup, the GC or atexit handlers + // may have already collected objects in this list, + // leaving dangling pointers. Attempting Py_DECREF on + // freed memory causes a SIGSEGV. g_greenlet_shutting_down + // covers the early atexit phase; Py_IsFinalizing() covers + // later phases. Thus, we deliberately leak. + if (greenlet::IsShuttingDown()) { + return; + } + + // Preserve any pending exception so that cleanup-triggered + // errors don't accidentally swallow an unrelated exception + // (e.g. one set by throw() before a switch). + PyErrPieces incoming_err; + + for(deleteme_t::iterator it = copy.begin(), end = copy.end(); + it != end; + ++it ) { + PyGreenlet* to_del = *it; + if (murder) { + // Force each greenlet to appear dead; we can't raise an + // exception into it anymore anyway. + to_del->pimpl->murder_in_place(); + } + + // The only reference to these greenlets should be in + // this list, decreffing them should let them be + // deleted again, triggering calls to green_dealloc() + // in the correct thread (if we're not murdering). + // This may run arbitrary Python code and switch + // threads or greenlets! + Py_DECREF(to_del); + if (PyErr_Occurred()) { + PyErr_WriteUnraisable(nullptr); + PyErr_Clear(); + } + } + // Not worried about C++ exception safety here in terms of + // making sure we restore the error. Either we'll catch it + // above and establish the error from that exception + // (which, yes, might overwrite something from before we + // entered, but we're in an undefined situation at that + // point) or we won't catch it at all and will crash the + // process. + // + // As for Python exception safety, there's no chance we're + // overwriting an exception (from the loop) with no + // exception (captured NULLs before we entered the loop), + // because there CAN'T BE any exception from the loop --- + // we clear them. So we're either restoring a pre-existing + // exception, or leaving the exception unset (by restoring + // NULL). + incoming_err.PyErrRestore(); + } + +public: + + /** + * Returns a new reference, or a false object. + */ + inline OwnedObject get_tracefunc() const + { + return tracefunc; + }; + + + inline void set_tracefunc(BorrowedObject tracefunc) + { + assert(tracefunc); + if (tracefunc == BorrowedObject(Py_None)) { + this->tracefunc.CLEAR(); + } + else { + this->tracefunc = tracefunc; + } + } + + /** + * Given a reference to a greenlet that some other thread + * attempted to delete (has a refcount of 0) store it for later + * deletion when the thread this state belongs to is current. + */ + inline void delete_when_thread_running(PyGreenlet* to_del) + { + Py_INCREF(to_del); +#ifdef Py_GIL_DISABLED + LockGuard deleteme_guard(this->deleteme_lock); +#endif + this->deleteme.push_back(to_del); + } + + /** + * Set to std::clock_t(-1) to disable. + */ + inline static std::clock_t clocks_used_doing_gc() + { +#ifdef Py_GIL_DISABLED + return ThreadState::_clocks_used_doing_gc.load(std::memory_order_relaxed); +#else + return ThreadState::_clocks_used_doing_gc; +#endif + } + + inline static void set_clocks_used_doing_gc(std::clock_t value) + { +#ifdef Py_GIL_DISABLED + ThreadState::_clocks_used_doing_gc.store(value, std::memory_order_relaxed); +#else + ThreadState::_clocks_used_doing_gc = value; +#endif + } + + inline static void add_clocks_used_doing_gc(std::clock_t value) + { +#ifdef Py_GIL_DISABLED + ThreadState::_clocks_used_doing_gc.fetch_add(value, std::memory_order_relaxed); +#else + ThreadState::_clocks_used_doing_gc += value; +#endif + } + + // Runs in some arbitrary thread that Python is using to invoke + // pending callbacks. This may not be the thread that was + // running the greenlets. + ~ThreadState() + { + if (!PyInterpreterState_Head()) { + // We shouldn't get here (our callers protect us) + // but if we do, all we can do is bail early. + return; + } + + // During interpreter finalization, Python APIs like + // PyImport_ImportModule are unsafe (the import machinery may + // be partially torn down). On Python < 3.11, perform only the + // minimal cleanup that is safe: clear our strong references + // so we don't leak, but skip the GC-based leak detection. + // + // Python 3.11+ restructured interpreter finalization so that + // these APIs remain safe during shutdown. + if (greenlet::IsShuttingDown()) { + this->tracefunc.CLEAR(); + if (this->current_greenlet) { + this->current_greenlet->murder_in_place(); + this->current_greenlet.CLEAR(); + } + this->main_greenlet.CLEAR(); + return; + } + + // We should not have an "origin" greenlet; that only exists + // for the temporary time during a switch, which should not + // be in progress as the thread dies. + //assert(!this->switching_state.origin); + + this->tracefunc.CLEAR(); + + // Forcibly GC as much as we can. + this->clear_deleteme_list(true); + + // The pending call did this. + assert(this->main_greenlet->thread_state() == nullptr); + + // If the main greenlet is the current greenlet, + // then we "fell off the end" and the thread died. + // It's possible that there is some other greenlet that + // switched to us, leaving a reference to the main greenlet + // on the stack, somewhere uncollectible. Try to detect that. + if (this->current_greenlet == this->main_greenlet && this->current_greenlet) { + assert( + this->current_greenlet->is_currently_running_in_some_thread() + || this->current_greenlet->was_running_in_dead_thread() + ); + // Drop one reference we hold. + this->current_greenlet.CLEAR(); + assert(!this->current_greenlet); + // Only our reference to the main greenlet should be left, + // But hold onto the pointer in case we need to do extra cleanup. + PyGreenlet* old_main_greenlet = this->main_greenlet.borrow(); + Py_ssize_t cnt = this->main_greenlet.REFCNT(); + this->main_greenlet.CLEAR(); + if (ThreadState::clocks_used_doing_gc() != std::clock_t(-1) + && cnt == 2 && Py_REFCNT(old_main_greenlet) == 1) { + // Highly likely that the reference is somewhere on + // the stack, not reachable by GC. Verify. + // XXX: This is O(n) in the total number of objects. + // TODO: Add a way to disable this at runtime, and + // another way to report on it. + std::clock_t begin = std::clock(); + NewReference gc(PyImport_ImportModule("gc")); + if (gc) { + OwnedObject get_referrers = gc.PyRequireAttr(ThreadState::get_referrers_name); + OwnedList refs(get_referrers.PyCall(old_main_greenlet)); + if (refs && refs.empty()) { + assert(refs.REFCNT() == 1); + // We found nothing! So we left a dangling + // reference: Probably the last thing some + // other greenlet did was call + // 'getcurrent().parent.switch()' to switch + // back to us. Clean it up. This will be the + // case on CPython 3.7 and newer, as they use + // an internal calling conversion that avoids + // creating method objects and storing them on + // the stack. + Py_DECREF(old_main_greenlet); + } + else if (refs + && refs.size() == 1 + && PyCFunction_Check(refs.at(0)) + && Py_REFCNT(refs.at(0)) == 2) { + assert(refs.REFCNT() == 1); + // Ok, we found a C method that refers to the + // main greenlet, and its only referenced + // twice, once in the list we just created, + // once from...somewhere else. If we can't + // find where else, then this is a leak. + // This happens in older versions of CPython + // that create a bound method object somewhere + // on the stack that we'll never get back to. + if (PyCFunction_GetFunction(refs.at(0).borrow()) == (PyCFunction)green_switch) { + BorrowedObject function_w = refs.at(0); + refs.clear(); // destroy the reference + // from the list. + // back to one reference. Can *it* be + // found? + assert(function_w.REFCNT() == 1); + refs = get_referrers.PyCall(function_w); + if (refs && refs.empty()) { + // Nope, it can't be found so it won't + // ever be GC'd. Drop it. + Py_CLEAR(function_w); + } + } + } + std::clock_t end = std::clock(); + ThreadState::add_clocks_used_doing_gc(end - begin); + } + } + } + + // We need to make sure this greenlet appears to be dead, + // because otherwise deallocing it would fail to raise an + // exception in it (the thread is dead) and put it back in our + // deleteme list. + if (this->current_greenlet) { + this->current_greenlet->murder_in_place(); + this->current_greenlet.CLEAR(); + } + + if (this->main_greenlet) { + // Couldn't have been the main greenlet that was running + // when the thread exited (because we already cleared this + // pointer if it was). This shouldn't be possible? + + // If the main greenlet was current when the thread died (it + // should be, right?) then we cleared its self pointer above + // when we cleared the current greenlet's main greenlet pointer. + // assert(this->main_greenlet->main_greenlet == this->main_greenlet + // || !this->main_greenlet->main_greenlet); + // // self reference, probably gone + // this->main_greenlet->main_greenlet.CLEAR(); + + // This will actually go away when the ivar is destructed. + this->main_greenlet.CLEAR(); + } + + if (PyErr_Occurred()) { + PyErr_WriteUnraisable(NULL); + PyErr_Clear(); + } + + } + +}; + +ImmortalString ThreadState::get_referrers_name(nullptr); +#ifdef Py_GIL_DISABLED +std::atomic ThreadState::_clocks_used_doing_gc(0); +#else +std::clock_t ThreadState::_clocks_used_doing_gc(0); +#endif + + + + + +}; // namespace greenlet + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp b/venv/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp new file mode 100644 index 0000000..b6d7d59 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp @@ -0,0 +1,105 @@ +#ifndef GREENLET_THREAD_STATE_CREATOR_HPP +#define GREENLET_THREAD_STATE_CREATOR_HPP + +#include +#include + +#include "greenlet_internal.hpp" +#include "greenlet_refs.hpp" +#include "greenlet_thread_support.hpp" + +#include "TThreadState.hpp" + +namespace greenlet { + + +typedef void (*ThreadStateDestructor)(ThreadState* const); + +// Only one of these, auto created per thread as a thread_local. +// This means we don't have to worry about atomic access to the +// internals, because by definition all access is happening on a +// single thread. +// Constructing the state constructs the MainGreenlet. +template +class ThreadStateCreator +{ +private: + // Initialized to 1, and, if still 1, created on access. + // Set to 0 on destruction. + ThreadState* _state; + G_NO_COPIES_OF_CLS(ThreadStateCreator); + + inline bool has_initialized_state() const noexcept + { + return this->_state != (ThreadState*)1; + } + + inline bool has_state() const noexcept + { + return this->has_initialized_state() && this->_state != nullptr; + } + +public: + + ThreadStateCreator() : + _state((ThreadState*)1) + { + } + + ~ThreadStateCreator() + { + if (this->has_state()) { + Destructor(this->_state); + } + + this->_state = nullptr; + } + + inline ThreadState& state() + { + // The main greenlet will own this pointer when it is created, + // which will be right after this. The plan is to give every + // greenlet a pointer to the main greenlet for the thread it + // runs in; if we are doing something cross-thread, we need to + // access the pointer from the main greenlet. Deleting the + // thread, and hence the thread-local storage, will delete the + // state pointer in the main greenlet. + if (!this->has_initialized_state()) { + // XXX: Assuming allocation never fails + this->_state = new ThreadState; + // For non-standard threading, we need to store an object + // in the Python thread state dictionary so that it can be + // DECREF'd when the thread ends (ideally; the dict could + // last longer) and clean this object up. + } + if (!this->_state) { + throw std::runtime_error("Accessing state after destruction."); + } + return *this->_state; + } + + operator ThreadState&() + { + return this->state(); + } + + operator ThreadState*() + { + return &this->state(); + } + + inline int tp_traverse(visitproc visit, void* arg) + { + if (this->has_state()) { + return this->_state->tp_traverse(visit, arg); + } + return 0; + } + +}; + + + +}; // namespace greenlet + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp b/venv/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp new file mode 100644 index 0000000..5562a73 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp @@ -0,0 +1,217 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of the ThreadState destructors. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_THREADSTATE_DESTROY +#define T_THREADSTATE_DESTROY + +#include "TGreenlet.hpp" + +#include "greenlet_thread_support.hpp" +#include "greenlet_compiler_compat.hpp" +#include "TGreenletGlobals.cpp" +#include "TThreadState.hpp" +#include "TThreadStateCreator.hpp" + +namespace greenlet { + +extern "C" { + +struct ThreadState_DestroyNoGIL +{ + /** + This function uses the same lock that the PendingCallback does + */ + static void + MarkGreenletDeadAndQueueCleanup(ThreadState* const state) + { +#if GREENLET_BROKEN_THREAD_LOCAL_CLEANUP_JUST_LEAK + // One rare platform. + return; +#endif + // We are *NOT* holding the GIL. Our thread is in the middle + // of its death throes and the Python thread state is already + // gone so we can't use most Python APIs. One that is safe is + // ``Py_AddPendingCall``, unless the interpreter itself has + // been torn down. There is a limited number of calls that can + // be queued: 32 (NPENDINGCALLS) in CPython 3.10, so we + // coalesce these calls using our own queue. + + if (!MarkGreenletDeadIfNeeded(state)) { + // No state, or no greenlet + return; + } + + // XXX: Because we don't have the GIL, this is a race condition. + if (!PyInterpreterState_Head()) { + // We have to leak the thread state, if the + // interpreter has shut down when we're getting + // deallocated, we can't run the cleanup code that + // deleting it would imply. + return; + } + + AddToCleanupQueue(state); + + } + +private: + + // If the state has an allocated main greenlet: + // - mark the greenlet as dead by disassociating it from the state; + // - return 1 + // Otherwise, return 0. + static bool + MarkGreenletDeadIfNeeded(ThreadState* const state) + { + if (!state) { + return false; + } + LockGuard cleanup_lock(*mod_globs->thread_states_to_destroy_lock); + // mark the thread as dead ASAP. + // TODO: While the state variable tracking the death is + // atomic, and used with the strictest memory ordering, could + // this still be hiding race conditions? Specifically, is + // there a scenario where a thread is dying and thread local + // variables are being deconstructed, and some other thread + // tries to switch/throw to a greenlet owned by this thread, + // such that we think the switch will work but it won't? + return state->mark_main_greenlet_dead(); + } + + static void + AddToCleanupQueue(ThreadState* const state) + { + assert(state && state->has_main_greenlet()); + + // NOTE: Because we're not holding the GIL here, some other + // Python thread could run and call ``os.fork()``, which would + // be bad if that happened while we are holding the cleanup + // lock (it wouldn't function in the child process). + // Make a best effort to try to keep the duration we hold the + // lock short. + // TODO: On platforms that support it, use ``pthread_atfork`` to + // drop this lock. + LockGuard cleanup_lock(*mod_globs->thread_states_to_destroy_lock); + + mod_globs->queue_to_destroy(state); + if (mod_globs->thread_states_to_destroy.size() == 1) { + // We added the first item to the queue. We need to schedule + // the cleanup. + + // A size greater than 1 means that we have already added the pending call, + // and in fact, it may be executing now. + // If it is executing, our lock makes sure that it will see the item we just added + // to the queue on its next iteration (after we release the lock) + // + // A size of 1 means there is no pending call, OR the pending call is + // currently executing, has dropped the lock, and is deleting the last item + // from the queue; its next iteration will go ahead and delete the item we just added. + // And the pending call we schedule here will have no work to do. + int result = AddPendingCall( + PendingCallback_DestroyQueue, + nullptr); + if (result < 0) { + // Hmm, what can we do here? + fprintf(stderr, + "greenlet: WARNING: failed in call to Py_AddPendingCall; " + "expect a memory leak.\n"); + } + } + } + + static int + PendingCallback_DestroyQueue(void* UNUSED(arg)) + { + // We're may or may not be holding the GIL here (depending on + // Py_GIL_DISABLED), so calls to ``os.fork()`` may or may not + // be possible. + while (1) { + ThreadState* to_destroy; + { + LockGuard cleanup_lock(*mod_globs->thread_states_to_destroy_lock); + if (mod_globs->thread_states_to_destroy.empty()) { + break; + } + to_destroy = mod_globs->take_next_to_destroy(); + } + assert(to_destroy); + assert(to_destroy->has_main_greenlet()); + // Drop the lock while we do the actual deletion. + // This allows other calls to MarkGreenletDeadAndQueueCleanup + // to enter and add to our queue. + DestroyOne(to_destroy); + } + return 0; + } + + static void + DestroyOne(const ThreadState* const state) + { + // May or may not be holding the GIL (depending on Py_GIL_DISABLED). + // Passed a non-shared pointer to the actual thread state. + // state -> main greenlet + // + // The thread_state in the main greenlet has already been + // cleared by the time this function runs from our pending + // callback, but the greenlet itself is still there. +#ifndef NDEBUG + PyGreenlet* main(state->borrow_main_greenlet()); + assert(main); + assert(main->pimpl->thread_state() == nullptr); +#endif + delete state; // Deleting this runs the destructor, DECREFs the main greenlet. + } + + + static int AddPendingCall(int (*func)(void*), void* arg) + { + // If the interpreter is in the middle of finalizing, we can't + // add a pending call. Trying to do so will end up in a + // SIGSEGV, as Py_AddPendingCall will not be able to get the + // interpreter and will try to dereference a NULL pointer. + // It's possible this can still segfault if we happen to get + // context switched, and maybe we should just always implement + // our own AddPendingCall, but I'd like to see if this works + // first + if (greenlet::IsShuttingDown()) { +#ifdef GREENLET_DEBUG + // No need to log in the general case. Yes, we'll leak, + // but we're shutting down so it should be ok. + fprintf(stderr, + "greenlet: WARNING: Interpreter is finalizing. Ignoring " + "call to Py_AddPendingCall; \n"); +#endif + return 0; + } + return Py_AddPendingCall(func, arg); + } + + + + + +}; +}; + +}; // namespace greenlet + +// The intent when GET_THREAD_STATE() is needed multiple times in a +// function is to take a reference to its return value in a local +// variable, to avoid the thread-local indirection. On some platforms +// (macOS), accessing a thread-local involves a function call (plus an +// initial function call in each function that uses a thread local); +// in contrast, static volatile variables are at some pre-computed +// offset. +typedef greenlet::ThreadStateCreator ThreadStateCreator; +static thread_local ThreadStateCreator g_thread_state_global; +#define GET_THREAD_STATE() g_thread_state_global + +#endif //T_THREADSTATE_DESTROY diff --git a/venv/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp b/venv/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp new file mode 100644 index 0000000..ce011fd --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp @@ -0,0 +1,677 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::UserGreenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_USER_GREENLET_CPP +#define T_USER_GREENLET_CPP + +#include "greenlet_internal.hpp" +#include "TGreenlet.hpp" + +#include "TThreadStateDestroy.cpp" + + +namespace greenlet { +using greenlet::refs::BorrowedMainGreenlet; +greenlet::PythonAllocator UserGreenlet::allocator; + +void* UserGreenlet::operator new(size_t UNUSED(count)) +{ + return allocator.allocate(1); +} + + +void UserGreenlet::operator delete(void* ptr) +{ + return allocator.deallocate(static_cast(ptr), + 1); +} + + +UserGreenlet::UserGreenlet(PyGreenlet* p, BorrowedGreenlet the_parent) + : Greenlet(p), _parent(the_parent) +{ +} + +UserGreenlet::~UserGreenlet() +{ + // Python 3.11: If we don't clear out the raw frame datastack + // when deleting an unfinished greenlet, + // TestLeaks.test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main fails. + this->python_state.did_finish(nullptr); + this->tp_clear(); +} + + +const BorrowedMainGreenlet +UserGreenlet::main_greenlet() const +{ + return this->_main_greenlet; +} + + +BorrowedMainGreenlet +UserGreenlet::find_main_greenlet_in_lineage() const +{ + if (this->started()) { + assert(this->_main_greenlet); + return BorrowedMainGreenlet(this->_main_greenlet); + } + + if (!this->_parent) { + /* garbage collected greenlet in chain */ + // XXX: WHAT? + return BorrowedMainGreenlet(nullptr); + } + + return this->_parent->find_main_greenlet_in_lineage(); +} + + +/** + * CAUTION: This will allocate memory and may trigger garbage + * collection and arbitrary Python code. + */ +OwnedObject +UserGreenlet::throw_GreenletExit_during_dealloc(const ThreadState& current_thread_state) +{ + /* The dying greenlet cannot be a parent of ts_current + because the 'parent' field chain would hold a + reference */ + UserGreenlet::ParentIsCurrentGuard with_current_parent(this, current_thread_state); + + // We don't care about the return value, only whether an + // exception happened. Whether or not an exception happens, + // we need to restore the parent in case the greenlet gets + // resurrected. + return Greenlet::throw_GreenletExit_during_dealloc(current_thread_state); +} + +ThreadState* +UserGreenlet::thread_state() const noexcept +{ + // TODO: maybe make this throw, if the thread state isn't there? + // if (!this->main_greenlet) { + // throw std::runtime_error("No thread state"); // TODO: Better exception + // } + if (!this->_main_greenlet) { + return nullptr; + } + return this->_main_greenlet->thread_state(); +} + + +bool +UserGreenlet::was_running_in_dead_thread() const noexcept +{ + return this->_main_greenlet && !this->thread_state(); +} + +OwnedObject +UserGreenlet::g_switch() +{ + try { + if (!this->args() && !PyErr_Occurred()) { + // we have nothing to send as the result of switching, + // most likely because we've somehow allowed concurrent + // uses of switch from multiple threads (which may or may + // not be allowed by check_switch_allowed) + // ``green_switch`` defends against this by calling + // ``check_switch_allowed`` before messing with + // ``args()``, but we have at least one internal caller + // (``throw_GreenletExit_during_dealloc``) so we keep both + // this explicit check and our call to + // ``check_switch_allowed`` + throw PyErrOccurred(mod_globs->PyExc_GreenletError, + "cannot switch with no pending arguments or exception"); + } + this->check_switch_allowed(); + } + catch (const PyErrOccurred&) { + this->release_args(); + throw; + } + + // Switching greenlets used to attempt to clean out ones that need + // deleted *if* we detected a thread switch. Should it still do + // that? + // An issue is that if we delete a greenlet from another thread, + // it gets queued to this thread, and ``kill_greenlet()`` switches + // back into the greenlet + + /* find the real target by ignoring dead greenlets, + and if necessary starting a greenlet. */ + switchstack_result_t err; + Greenlet* target = this; + // TODO: probably cleaner to handle the case where we do + // switch to ourself separately from the other cases. + // This can probably even further be simplified if we keep + // track of the switching_state we're going for and just call + // into g_switch() if it's not ourself. The main problem with that + // is that we would be using more stack space. + bool target_was_me = true; + bool was_initial_stub = false; + while (target) { + if (target->active()) { + if (!target_was_me) { + target->args() <<= this->args(); + assert(!this->args()); + } + err = target->g_switchstack(); + break; + } + if (!target->started()) { + // We never encounter a main greenlet that's not started. + assert(!target->main()); + UserGreenlet* real_target = static_cast(target); + assert(real_target); + void* dummymarker; + was_initial_stub = true; + if (!target_was_me) { + target->args() <<= this->args(); + assert(!this->args()); + } + try { + // This can only throw back to us while we're + // still in this greenlet. Once the new greenlet + // is bootstrapped, it has its own exception state. + err = real_target->g_initialstub(&dummymarker); + } + catch (const PyErrOccurred&) { + this->release_args(); + throw; + } + catch (const GreenletStartedWhileInPython&) { + // The greenlet was started sometime before this + // greenlet actually switched to it, i.e., + // "concurrent" calls to switch() or throw(). + // We need to retry the switch. + // Note that the current greenlet has been reset + // to this one (or we wouldn't be running!) + continue; + } + break; + } + + target = target->parent(); + target_was_me = false; + } + // The ``this`` pointer and all other stack or register based + // variables are invalid now, at least where things succeed + // above. + // But this one, probably not so much? It's not clear if it's + // safe to throw an exception at this point. + + if (err.status < 0) { + // If we get here, either g_initialstub() + // failed, or g_switchstack() failed. Either one of those + // cases SHOULD leave us in the original greenlet with a valid + // stack. + return this->on_switchstack_or_initialstub_failure(target, err, target_was_me, was_initial_stub); + } + + // err.the_new_current_greenlet would be the same as ``target``, + // if target wasn't probably corrupt. + return err.the_new_current_greenlet->g_switch_finish(err); +} + + + +Greenlet::switchstack_result_t +UserGreenlet::g_initialstub(void* mark) +{ + OwnedObject run; + + // We need to grab a reference to the current switch arguments + // in case we're entered concurrently during the call to + // GetAttr() and have to try again. + // We'll restore them when we return in that case. + // Scope them tightly to avoid ref leaks. + { + SwitchingArgs args(this->args()); + + /* save exception in case getattr clears it */ + PyErrPieces saved; + + /* + self.run is the object to call in the new greenlet. + This could run arbitrary python code and switch greenlets! + */ + run = this->self().PyRequireAttr(mod_globs->str_run); + /* restore saved exception */ + saved.PyErrRestore(); + + + /* recheck that it's safe to switch in case greenlet reparented anywhere above */ + this->check_switch_allowed(); + + /* by the time we got here another start could happen elsewhere, + * that means it should now be a regular switch. + * This can happen if the Python code is a subclass that implements + * __getattribute__ or __getattr__, or makes ``run`` a descriptor; + * all of those can run arbitrary code that switches back into + * this greenlet. + */ + if (this->stack_state.started()) { + // the successful switch cleared these out, we need to + // restore our version. They will be copied on up to the + // next target. + assert(!this->args()); + this->args() <<= args; + throw GreenletStartedWhileInPython(); + } + } + + // Sweet, if we got here, we have the go-ahead and will switch + // greenlets. + // Nothing we do from here on out should allow for a thread or + // greenlet switch: No arbitrary calls to Python, including + // decref'ing + +#if GREENLET_USE_CFRAME + /* OK, we need it, we're about to switch greenlets, save the state. */ + /* + See green_new(). This is a stack-allocated variable used + while *self* is in PyObject_Call(). + We want to defer copying the state info until we're sure + we need it and are in a stable place to do so. + */ + _PyCFrame trace_info; + + this->python_state.set_new_cframe(trace_info); +#endif + /* start the greenlet */ + ThreadState& thread_state = GET_THREAD_STATE().state(); + this->stack_state = StackState(mark, + thread_state.borrow_current()->stack_state); + this->python_state.set_initial_state(PyThreadState_GET()); + this->exception_state.clear(); + this->_main_greenlet = thread_state.get_main_greenlet(); + + /* perform the initial switch */ + switchstack_result_t err = this->g_switchstack(); + /* returns twice! + The 1st time with ``err == 1``: we are in the new greenlet. + This one owns a greenlet that used to be current. + The 2nd time with ``err <= 0``: back in the caller's + greenlet; this happens if the child finishes or switches + explicitly to us. Either way, the ``err`` variable is + created twice at the same memory location, but possibly + having different ``origin`` values. Note that it's not + constructed for the second time until the switch actually happens. + */ + if (err.status == 1) { + // In the new greenlet. + + // This never returns! Calling inner_bootstrap steals + // the contents of our run object within this stack frame, so + // it is not valid to do anything with it. + try { + this->inner_bootstrap(err.origin_greenlet.relinquish_ownership(), + run.relinquish_ownership()); + } + // Getting a C++ exception here isn't good. It's probably a + // bug in the underlying greenlet, meaning it's probably a + // C++ extension. We're going to abort anyway, but try to + // display some nice information *if* possible. Some obscure + // platforms don't properly support this (old 32-bit Arm, see see + // https://github.com/python-greenlet/greenlet/issues/385); that's not + // great, but should usually be OK because, as mentioned above, we're + // terminating anyway. + // + // The catching is tested by + // ``test_cpp.CPPTests.test_unhandled_exception_in_greenlet_aborts``. + // + // PyErrOccurred can theoretically be thrown by + // inner_bootstrap() -> g_switch_finish(), but that should + // never make it back to here. It is a std::exception and + // would be caught if it is. + catch (const std::exception& e) { + std::string base = "greenlet: Unhandled C++ exception: "; + base += e.what(); + Py_FatalError(base.c_str()); + } + catch (...) { + // Some compilers/runtimes use exceptions internally. + // It appears that GCC on Linux with libstdc++ throws an + // exception internally at process shutdown time to unwind + // stacks and clean up resources. Depending on exactly + // where we are when the process exits, that could result + // in an unknown exception getting here. If we + // Py_FatalError() or abort() here, we interfere with + // orderly process shutdown. Throwing the exception on up + // is the right thing to do. + // + // gevent's ``examples/dns_mass_resolve.py`` demonstrates this. +#ifndef NDEBUG + fprintf(stderr, + "greenlet: inner_bootstrap threw unknown exception; " + "is the process terminating?\n"); +#endif + throw; + } + Py_FatalError("greenlet: inner_bootstrap returned with no exception.\n"); + } + + + // In contrast, notice that we're keeping the origin greenlet + // around as an owned reference; we need it to call the trace + // function for the switch back into the parent. It was only + // captured at the time the switch actually happened, though, + // so we haven't been keeping an extra reference around this + // whole time. + + /* back in the parent */ + if (err.status < 0) { + /* start failed badly, restore greenlet state */ + this->stack_state = StackState(); + this->_main_greenlet.CLEAR(); + // CAUTION: This may run arbitrary Python code. + run.CLEAR(); // inner_bootstrap didn't run, we own the reference. + } + + // In the success case, the spawned code (inner_bootstrap) will + // take care of decrefing this, so we relinquish ownership so as + // to not double-decref. + + run.relinquish_ownership(); + + return err; +} + + +void +UserGreenlet::inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run) +{ + // The arguments here would be another great place for move. + // As it is, we take them as a reference so that when we clear + // them we clear what's on the stack above us. Do that NOW, and + // without using a C++ RAII object, + // so there's no way that exiting the parent frame can clear it, + // or we clear it unexpectedly. This arises in the context of the + // interpreter shutting down. See https://github.com/python-greenlet/greenlet/issues/325 + //PyObject* run = _run.relinquish_ownership(); + + /* in the new greenlet */ + assert(this->thread_state()->borrow_current() == BorrowedGreenlet(this->_self)); + // C++ exceptions cannot propagate to the parent greenlet from + // here. (TODO: Do we need a catch(...) clause, perhaps on the + // function itself? ALl we could do is terminate the program.) + // NOTE: On 32-bit Windows, the call chain is extremely + // important here in ways that are subtle, having to do with + // the depth of the SEH list. The call to restore it MUST NOT + // add a new SEH handler to the list, or we'll restore it to + // the wrong thing. + this->thread_state()->restore_exception_state(); + /* stack variables from above are no good and also will not unwind! */ + // EXCEPT: That can't be true, we access run, among others, here. + + this->stack_state.set_active(); /* running */ + + // We're about to possibly run Python code again, which + // could switch back/away to/from us, so we need to grab the + // arguments locally. + SwitchingArgs args; + args <<= this->args(); + assert(!this->args()); + + this->_run_callable.CLEAR(); + // stash the run callable in this->_run_callable to ensure that GC will be + // able to find the object later. + // This is needed for the case of a permanently suspended greenlet + // so that the run callable is not leaked. + this->_run_callable.steal(run); + run = nullptr; + + + // The first switch we need to manually call the trace + // function here instead of in g_switch_finish, because we + // never return there. + if (OwnedObject tracefunc = this->thread_state()->get_tracefunc()) { + OwnedGreenlet trace_origin; + trace_origin = origin_greenlet; + try { + g_calltrace(tracefunc, + args ? mod_globs->event_switch : mod_globs->event_throw, + trace_origin, + this->_self); + } + catch (const PyErrOccurred&) { + /* Turn trace errors into switch throws */ + args.CLEAR(); + } + } + + // We no longer need the origin, it was only here for + // tracing. + // We may never actually exit this stack frame so we need + // to explicitly clear it. + // This could run Python code and switch. + Py_CLEAR(origin_greenlet); + + OwnedObject result; + if (!args) { + /* pending exception */ + result = NULL; + } + else { + /* call g.run(*args, **kwargs) */ + // This could result in further switches + try { + //result = run.PyCall(args.args(), args.kwargs()); + // CAUTION: Just invoking this, before the function even + // runs, may cause memory allocations, which may trigger + // GC, which may run arbitrary Python code. + result = OwnedObject::consuming(PyObject_Call(this->_run_callable.borrow(), args.args().borrow(), args.kwargs().borrow())); + } + catch (...) { + // Unhandled C++ exception! + + // If we declare ourselves as noexcept, if we don't catch + // this here, most platforms will just abort() the + // process. But on 64-bit Windows with older versions of + // the C runtime, this can actually corrupt memory and + // just return. We see this when compiling with the + // Windows 7.0 SDK targeting Windows Server 2008, but not + // when using the Appveyor Visual Studio 2019 image. So + // this currently only affects Python 2.7 on Windows 64. + // That is, the tests pass and the runtime aborts + // everywhere else. + // + // However, if we catch it and try to continue with a + // Python error, then all Windows 64 bit platforms corrupt + // memory. So all we can do is manually abort, hopefully + // with a good error message. (Note that the above was + // tested WITHOUT the `/EHr` switch being used at compile + // time, so MSVC may have "optimized" out important + // checking. Using that switch, we may be in a better + // place in terms of memory corruption.) But sometimes it + // can't be caught here at all, which is confusing but not + // terribly surprising; so again, the G_NOEXCEPT_WIN32 + // plus "/EHr". + // + // Hopefully the basic C stdlib is still functional enough + // for us to at least print an error. + // + // It gets more complicated than that, though, on some + // platforms, specifically at least Linux/gcc/libstdc++. They use + // an exception to unwind the stack when a background + // thread exits. (See comments about noexcept.) So this + // may not actually represent anything untoward. On those + // platforms we allow throws of this to propagate, or + // attempt to anyway. +# if defined(WIN32) || defined(_WIN32) + Py_FatalError( + "greenlet: Unhandled C++ exception from a greenlet run function. " + "Because memory is likely corrupted, terminating process."); + std::abort(); +#else + throw; +#endif + } + } + // These lines may run arbitrary code + args.CLEAR(); + assert(run == nullptr); + this->_run_callable.CLEAR(); + + if (!result + && mod_globs->PyExc_GreenletExit.PyExceptionMatches() + && (this->args())) { + // This can happen, for example, if our only reference + // goes away after we switch back to the parent. + // See test_dealloc_switch_args_not_lost + PyErrPieces clear_error; + result <<= this->args(); + result = single_result(result); + } + this->release_args(); + this->python_state.did_finish(PyThreadState_GET()); + + result = g_handle_exit(result); + assert(this->thread_state()->borrow_current() == this->_self); + + /* jump back to parent */ + this->stack_state.set_inactive(); /* dead */ + + + // TODO: Can we decref some things here? Release our main greenlet + // and maybe parent? + for (Greenlet* parent = this->_parent; + parent; + parent = parent->parent()) { + // We need to somewhere consume a reference to + // the result; in most cases we'll never have control + // back in this stack frame again. Calling + // green_switch actually adds another reference! + // This would probably be clearer with a specific API + // to hand results to the parent. + parent->args() <<= result; + assert(!result); + // The parent greenlet now owns the result; in the + // typical case we'll never get back here to assign to + // result and thus release the reference. + try { + result = parent->g_switch(); + } + catch (const PyErrOccurred&) { + // Ignore, keep passing the error on up. + } + + /* Return here means switch to parent failed, + * in which case we throw *current* exception + * to the next parent in chain. + */ + assert(!result); + } + /* We ran out of parents, cannot continue */ + PyErr_WriteUnraisable(this->self().borrow_o()); + Py_FatalError("greenlet: ran out of parent greenlets while propagating exception; " + "cannot continue"); + std::abort(); +} + +void +UserGreenlet::run(const BorrowedObject nrun) +{ + if (this->started()) { + throw AttributeError( + "run cannot be set " + "after the start of the greenlet"); + } + this->_run_callable = nrun; +} + +const OwnedGreenlet +UserGreenlet::parent() const +{ + return this->_parent; +} + +void +UserGreenlet::parent(const BorrowedObject raw_new_parent) +{ + if (!raw_new_parent) { + throw AttributeError("can't delete attribute"); + } + + BorrowedMainGreenlet main_greenlet_of_new_parent; + BorrowedGreenlet new_parent(raw_new_parent.borrow()); // could + // throw + // TypeError! + for (BorrowedGreenlet p = new_parent; p; p = p->parent()) { + if (p == this->self()) { + throw ValueError("cyclic parent chain"); + } + main_greenlet_of_new_parent = p->main_greenlet(); + } + + if (!main_greenlet_of_new_parent) { + throw ValueError("parent must not be garbage collected"); + } + + if (this->started() + && this->_main_greenlet != main_greenlet_of_new_parent) { + throw ValueError("parent cannot be on a different thread"); + } + + this->_parent = new_parent; +} + +void +UserGreenlet::murder_in_place() +{ + this->_main_greenlet.CLEAR(); + Greenlet::murder_in_place(); +} + +bool +UserGreenlet::belongs_to_thread(const ThreadState* thread_state) const +{ + return Greenlet::belongs_to_thread(thread_state) && this->_main_greenlet == thread_state->borrow_main_greenlet(); +} + + +int +UserGreenlet::tp_traverse(visitproc visit, void* arg) +{ + Py_VISIT(this->_parent.borrow_o()); + Py_VISIT(this->_main_greenlet.borrow_o()); + Py_VISIT(this->_run_callable.borrow_o()); + + return Greenlet::tp_traverse(visit, arg); +} + +int +UserGreenlet::tp_clear() +{ + Greenlet::tp_clear(); + this->_parent.CLEAR(); + this->_main_greenlet.CLEAR(); + this->_run_callable.CLEAR(); + return 0; +} + +UserGreenlet::ParentIsCurrentGuard::ParentIsCurrentGuard(UserGreenlet* p, + const ThreadState& thread_state) + : oldparent(p->_parent), + greenlet(p) +{ + p->_parent = thread_state.get_current(); +} + +UserGreenlet::ParentIsCurrentGuard::~ParentIsCurrentGuard() +{ + this->greenlet->_parent = oldparent; + oldparent.CLEAR(); +} + +}; //namespace greenlet +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/__init__.py b/venv/lib/python3.11/site-packages/greenlet/__init__.py new file mode 100644 index 0000000..e8bb3ad --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/__init__.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +""" +The root of the greenlet package. +""" + +__all__ = [ + '__version__', + '_C_API', + + 'GreenletExit', + 'error', + + 'getcurrent', + 'greenlet', + + 'gettrace', + 'settrace', +] + +# pylint:disable=no-name-in-module + +### +# Metadata +### +__version__ = '3.5.6' +from ._greenlet import _C_API # pylint:disable=no-name-in-module + +### +# Exceptions +### +from ._greenlet import GreenletExit +from ._greenlet import error + +### +# greenlets +### +from ._greenlet import getcurrent +from ._greenlet import greenlet + +### +# tracing +### +from ._greenlet import gettrace +from ._greenlet import settrace + +### +# Constants +# These constants aren't documented and aren't recommended. +# In 1.0, USE_GC and USE_TRACING are always true, and USE_CONTEXT_VARS +# is the same as ``sys.version_info[:2] >= 3.7`` +### +from ._greenlet import GREENLET_USE_CONTEXT_VARS # pylint:disable=unused-import +from ._greenlet import GREENLET_USE_GC # pylint:disable=unused-import +from ._greenlet import GREENLET_USE_TRACING # pylint:disable=unused-import + +# Controlling the use of the gc module. Provisional API for this greenlet +# implementation in 2.0. +from ._greenlet import CLOCKS_PER_SEC # pylint:disable=unused-import +from ._greenlet import enable_optional_cleanup # pylint:disable=unused-import +from ._greenlet import get_clocks_used_doing_optional_cleanup # pylint:disable=unused-import + +# Other APIS in the _greenlet module are for test support. diff --git a/venv/lib/python3.11/site-packages/greenlet/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..48aa47e Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..8bd00cd Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet.cpp b/venv/lib/python3.11/site-packages/greenlet/greenlet.cpp new file mode 100644 index 0000000..02dfa94 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet.cpp @@ -0,0 +1,324 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/* Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#include +#include +#include +#include + + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" // PyMemberDef + +#include "greenlet_internal.hpp" +// Code after this point can assume access to things declared in stdint.h, +// including the fixed-width types. This goes for the platform-specific switch functions +// as well. +#include "greenlet_refs.hpp" +#include "greenlet_slp_switch.hpp" + +#include "greenlet_thread_support.hpp" +#include "TGreenlet.hpp" + +#include "TGreenletGlobals.cpp" + +#include "TGreenlet.cpp" +#include "TMainGreenlet.cpp" +#include "TUserGreenlet.cpp" +#include "TBrokenGreenlet.cpp" +#include "TExceptionState.cpp" +#include "TPythonState.cpp" +#include "TStackState.cpp" + +#include "TThreadState.hpp" +#include "TThreadStateCreator.hpp" +#include "TThreadStateDestroy.cpp" + +#include "PyGreenlet.cpp" +#include "PyGreenletUnswitchable.cpp" +#include "CObjects.cpp" + +using greenlet::LockGuard; +using greenlet::LockInitError; +using greenlet::PyErrOccurred; +using greenlet::Require; + +using greenlet::g_handle_exit; +using greenlet::single_result; + +using greenlet::Greenlet; +using greenlet::UserGreenlet; +using greenlet::MainGreenlet; +using greenlet::BrokenGreenlet; +using greenlet::ThreadState; +using greenlet::PythonState; + + + +// ******* Implementation of things from included files +template +greenlet::refs::_BorrowedGreenlet& greenlet::refs::_BorrowedGreenlet::operator=(const greenlet::refs::BorrowedObject& other) +{ + this->_set_raw_pointer(static_cast(other)); + return *this; +} + +template +inline greenlet::refs::_BorrowedGreenlet::operator Greenlet*() const noexcept +{ + if (!this->p) { + return nullptr; + } + return reinterpret_cast(this->p)->pimpl; +} + +template +greenlet::refs::_BorrowedGreenlet::_BorrowedGreenlet(const BorrowedObject& p) + : BorrowedReference(nullptr) +{ + + this->_set_raw_pointer(p.borrow()); +} + +template +inline greenlet::refs::_OwnedGreenlet::operator Greenlet*() const noexcept +{ + if (!this->p) { + return nullptr; + } + return reinterpret_cast(this->p)->pimpl; +} + + + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wmissing-field-initializers" +# pragma clang diagnostic ignored "-Wwritable-strings" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +// warning: ISO C++ forbids converting a string constant to ‘char*’ +// (The python APIs aren't const correct and accept writable char*) +# pragma GCC diagnostic ignored "-Wwrite-strings" +#endif + + +/*********************************************************** + +A PyGreenlet is a range of C stack addresses that must be +saved and restored in such a way that the full range of the +stack contains valid data when we switch to it. + +Stack layout for a greenlet: + + | ^^^ | + | older data | + | | + stack_stop . |_______________| + . | | + . | greenlet data | + . | in stack | + . * |_______________| . . _____________ stack_copy + stack_saved + . | | | | + . | data | |greenlet data| + . | unrelated | | saved | + . | to | | in heap | + stack_start . | this | . . |_____________| stack_copy + | greenlet | + | | + | newer data | + | vvv | + + +Note that a greenlet's stack data is typically partly at its correct +place in the stack, and partly saved away in the heap, but always in +the above configuration: two blocks, the more recent one in the heap +and the older one still in the stack (either block may be empty). + +Greenlets are chained: each points to the previous greenlet, which is +the one that owns the data currently in the C stack above my +stack_stop. The currently running greenlet is the first element of +this chain. The main (initial) greenlet is the last one. Greenlets +whose stack is entirely in the heap can be skipped from the chain. + +The chain is not related to execution order, but only to the order +in which bits of C stack happen to belong to greenlets at a particular +point in time. + +The main greenlet doesn't have a stack_stop: it is responsible for the +complete rest of the C stack, and we don't know where it begins. We +use (char*) -1, the largest possible address. + +States: + stack_stop == NULL && stack_start == NULL: did not start yet + stack_stop != NULL && stack_start == NULL: already finished + stack_stop != NULL && stack_start != NULL: active + +The running greenlet's stack_start is undefined but not NULL. + + ***********************************************************/ + + + + +/***********************************************************/ + +/* Some functions must not be inlined: + * slp_restore_state, when inlined into slp_switch might cause + it to restore stack over its own local variables + * slp_save_state, when inlined would add its own local + variables to the saved stack, wasting space + * slp_switch, cannot be inlined for obvious reasons + * g_initialstub, when inlined would receive a pointer into its + own stack frame, leading to incomplete stack save/restore + +g_initialstub is a member function and declared virtual so that the +compiler always calls it through a vtable. + +slp_save_state and slp_restore_state are also member functions. They +are called from trampoline functions that themselves are declared as +not eligible for inlining. +*/ + +extern "C" { +static int GREENLET_NOINLINE(slp_save_state_trampoline)(char* stackref) +{ + return switching_thread_state->slp_save_state(stackref); +} +static void GREENLET_NOINLINE(slp_restore_state_trampoline)() +{ + switching_thread_state->slp_restore_state(); +} +} + + +/***********************************************************/ + + +#include "PyModule.cpp" + + + +static PyObject* +greenlet_internal_mod_init() noexcept +{ + static void* _PyGreenlet_API[PyGreenlet_API_pointers]; + + try { + CreatedModule m(greenlet_module_def); + + Require(PyType_Ready(&PyGreenlet_Type)); + Require(PyType_Ready(&PyGreenletUnswitchable_Type)); + + mod_globs = new greenlet::GreenletGlobals; + ThreadState::init(); + + m.PyAddObject("greenlet", PyGreenlet_Type); + m.PyAddObject("UnswitchableGreenlet", PyGreenletUnswitchable_Type); + m.PyAddObject("error", mod_globs->PyExc_GreenletError); + m.PyAddObject("GreenletExit", mod_globs->PyExc_GreenletExit); + + m.PyAddObject("GREENLET_USE_GC", 1); + m.PyAddObject("GREENLET_USE_TRACING", 1); + m.PyAddObject("GREENLET_USE_CONTEXT_VARS", 1L); + m.PyAddObject("GREENLET_USE_STANDARD_THREADING", 1L); + + NewReference clocks_per_sec(Require(PyLong_FromSsize_t(CLOCKS_PER_SEC))); + m.PyAddObject("CLOCKS_PER_SEC", clocks_per_sec); + + /* also publish module-level data as attributes of the greentype. */ + // XXX: This is weird, and enables a strange pattern of + // confusing the class greenlet with the module greenlet; with + // the exception of (possibly) ``getcurrent()``, this + // shouldn't be encouraged so don't add new items here. + for (const char* const* p = copy_on_greentype; *p; p++) { + OwnedObject o = m.PyRequireAttr(*p); + Require(PyDict_SetItemString(PyGreenlet_Type.tp_dict, *p, o.borrow())); + } + + /* + * Expose C API + */ + + /* types */ + _PyGreenlet_API[PyGreenlet_Type_NUM] = (void*)&PyGreenlet_Type; + + /* exceptions */ + _PyGreenlet_API[PyExc_GreenletError_NUM] = (void*)mod_globs->PyExc_GreenletError; + _PyGreenlet_API[PyExc_GreenletExit_NUM] = (void*)mod_globs->PyExc_GreenletExit; + + /* methods */ + _PyGreenlet_API[PyGreenlet_New_NUM] = (void*)PyGreenlet_New; + _PyGreenlet_API[PyGreenlet_GetCurrent_NUM] = (void*)PyGreenlet_GetCurrent; + _PyGreenlet_API[PyGreenlet_Throw_NUM] = (void*)PyGreenlet_Throw; + _PyGreenlet_API[PyGreenlet_Switch_NUM] = (void*)PyGreenlet_Switch; + _PyGreenlet_API[PyGreenlet_SetParent_NUM] = (void*)PyGreenlet_SetParent; + + /* Previously macros, but now need to be functions externally. */ + _PyGreenlet_API[PyGreenlet_MAIN_NUM] = (void*)Extern_PyGreenlet_MAIN; + _PyGreenlet_API[PyGreenlet_STARTED_NUM] = (void*)Extern_PyGreenlet_STARTED; + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM] = (void*)Extern_PyGreenlet_ACTIVE; + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM] = (void*)Extern_PyGreenlet_GET_PARENT; + + /* XXX: Note that our module name is ``greenlet._greenlet``, but for + backwards compatibility with existing C code, we need the _C_API to + be directly in greenlet. + */ + const NewReference c_api_object(Require( + PyCapsule_New( + (void*)_PyGreenlet_API, + "greenlet._C_API", + NULL))); + m.PyAddObject("_C_API", c_api_object); + assert(c_api_object.REFCNT() == 2); + + // cerr << "Sizes:" + // << "\n\tGreenlet : " << sizeof(Greenlet) + // << "\n\tUserGreenlet : " << sizeof(UserGreenlet) + // << "\n\tMainGreenlet : " << sizeof(MainGreenlet) + // << "\n\tExceptionState : " << sizeof(greenlet::ExceptionState) + // << "\n\tPythonState : " << sizeof(greenlet::PythonState) + // << "\n\tStackState : " << sizeof(greenlet::StackState) + // << "\n\tSwitchingArgs : " << sizeof(greenlet::SwitchingArgs) + // << "\n\tOwnedObject : " << sizeof(greenlet::refs::OwnedObject) + // << "\n\tBorrowedObject : " << sizeof(greenlet::refs::BorrowedObject) + // << "\n\tPyGreenlet : " << sizeof(PyGreenlet) + // << endl; + +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m.borrow(), Py_MOD_GIL_NOT_USED); +#endif + + return m.borrow(); // But really it's the main reference. + } + catch (const LockInitError& e) { + PyErr_SetString(PyExc_MemoryError, e.what()); + return NULL; + } + catch (const PyErrOccurred&) { + return NULL; + } + +} + +extern "C" { + +PyMODINIT_FUNC +PyInit__greenlet(void) +{ + return greenlet_internal_mod_init(); +} + +}; // extern C + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet.h b/venv/lib/python3.11/site-packages/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp new file mode 100644 index 0000000..1cd9223 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp @@ -0,0 +1,76 @@ +#ifndef GREENLET_ALLOCATOR_HPP +#define GREENLET_ALLOCATOR_HPP + +#define PY_SSIZE_T_CLEAN +#include +#include +#include "greenlet_compiler_compat.hpp" +#include "greenlet_cpython_compat.hpp" + + +namespace greenlet +{ + // This allocator is stateless; all instances are identical. + // It can *ONLY* be used when we're sure we're holding the GIL + // (Python's allocators require the GIL). + template + struct PythonAllocator : public std::allocator { + + PythonAllocator(const PythonAllocator& UNUSED(other)) + : std::allocator() + { + } + + PythonAllocator(const std::allocator other) + : std::allocator(other) + {} + + template + PythonAllocator(const std::allocator& other) + : std::allocator(other) + { + } + + PythonAllocator() : std::allocator() {} + + T* allocate(size_t number_objects, const void* UNUSED(hint)=0) + { + void* p; + if (number_objects == 1) { +#ifdef Py_GIL_DISABLED + p = PyMem_Malloc(sizeof(T) * number_objects); +#else + p = PyObject_Malloc(sizeof(T)); +#endif + } + else { + p = PyMem_Malloc(sizeof(T) * number_objects); + } + return static_cast(p); + } + + void deallocate(T* t, size_t n) + { + void* p = t; + if (n == 1) { +#ifdef Py_GIL_DISABLED + PyMem_Free(p); +#else + PyObject_Free(p); +#endif + } + else { + PyMem_Free(p); + } + } + // This member is deprecated in C++17 and removed in C++20 + template< class U > + struct rebind { + typedef PythonAllocator other; + }; + + }; + +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp new file mode 100644 index 0000000..af24bd8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp @@ -0,0 +1,98 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef GREENLET_COMPILER_COMPAT_HPP +#define GREENLET_COMPILER_COMPAT_HPP + +/** + * Definitions to aid with compatibility with different compilers. + * + * .. caution:: Use extreme care with noexcept. + * Some compilers and runtimes, specifically gcc/libgcc/libstdc++ on + * Linux, implement stack unwinding by throwing an uncatchable + * exception, one that specifically does not appear to be an active + * exception to the rest of the runtime. If this happens while we're in a noexcept function, + * we have violated our dynamic exception contract, and so the runtime + * will call std::terminate(), which kills the process with the + * unhelpful message "terminate called without an active exception". + * + * This has happened in this scenario: A background thread is running + * a greenlet that has made a native call and released the GIL. + * Meanwhile, the main thread finishes and starts shutting down the + * interpreter. When the background thread is scheduled again and + * attempts to obtain the GIL, it notices that the interpreter is + * exiting and calls ``pthread_exit()``. This in turn starts to unwind + * the stack by throwing that exception. But we had the ``PyCall`` + * functions annotated as noexcept, so the runtime terminated us. + * + * #2 0x00007fab26fec2b7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6 + * #3 0x00007fab26febb3c in __gxx_personality_v0 () from /lib/x86_64-linux-gnu/libstdc++.so.6 + * #4 0x00007fab26f34de6 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1 + * #6 0x00007fab276a34c6 in __GI___pthread_unwind at ./nptl/unwind.c:130 + * #7 0x00007fab2769bd3a in __do_cancel () at ../sysdeps/nptl/pthreadP.h:280 + * #8 __GI___pthread_exit (value=value@entry=0x0) at ./nptl/pthread_exit.c:36 + * #9 0x000000000052e567 in PyThread_exit_thread () at ../Python/thread_pthread.h:370 + * #10 0x00000000004d60b5 in take_gil at ../Python/ceval_gil.h:224 + * #11 0x00000000004d65f9 in PyEval_RestoreThread at ../Python/ceval.c:467 + * #12 0x000000000060cce3 in setipaddr at ../Modules/socketmodule.c:1203 + * #13 0x00000000006101cd in socket_gethostbyname + */ + +#include + +# define G_NO_COPIES_OF_CLS(Cls) private: \ + Cls(const Cls& other) = delete; \ + Cls& operator=(const Cls& other) = delete + +# define G_NO_ASSIGNMENT_OF_CLS(Cls) private: \ + Cls& operator=(const Cls& other) = delete + +# define G_NO_COPY_CONSTRUCTOR_OF_CLS(Cls) private: \ + Cls(const Cls& other) = delete; + + +// CAUTION: MSVC is stupidly picky: +// +// "The compiler ignores, without warning, any __declspec keywords +// placed after * or & and in front of the variable identifier in a +// declaration." +// (https://docs.microsoft.com/en-us/cpp/cpp/declspec?view=msvc-160) +// +// So pointer return types must be handled differently (because of the +// trailing *), or you get inscrutable compiler warnings like "error +// C2059: syntax error: ''" +// +// In C++ 11, there is a standard syntax for attributes, and +// GCC defines an attribute to use with this: [[gnu:noinline]]. +// In the future, this is expected to become standard. + +#if defined(__GNUC__) || defined(__clang__) +/* We used to check for GCC 4+ or 3.4+, but those compilers are + laughably out of date. Just assume they support it. */ +# define GREENLET_NOINLINE(name) __attribute__((noinline)) name +# define GREENLET_NOINLINE_P(rtype, name) rtype __attribute__((noinline)) name +# define UNUSED(x) UNUSED_ ## x __attribute__((__unused__)) +#elif defined(_MSC_VER) +/* We used to check for && (_MSC_VER >= 1300) but that's also out of date. */ +# define GREENLET_NOINLINE(name) __declspec(noinline) name +# define GREENLET_NOINLINE_P(rtype, name) __declspec(noinline) rtype name +# define UNUSED(x) UNUSED_ ## x +#endif + +#if defined(_MSC_VER) +# define G_NOEXCEPT_WIN32 noexcept +#else +# define G_NOEXCEPT_WIN32 +#endif + +#if defined(__GNUC__) && defined(__POWERPC__) && defined(__APPLE__) +// 32-bit PPC/MacOSX. Only known to be tested on unreleased versions +// of macOS 10.6 using a macports build gcc 14. It appears that +// running C++ destructors of thread-local variables is broken. + +// See https://github.com/python-greenlet/greenlet/pull/419 +# define GREENLET_BROKEN_THREAD_LOCAL_CLEANUP_JUST_LEAK 1 +#else +# define GREENLET_BROKEN_THREAD_LOCAL_CLEANUP_JUST_LEAK 0 +#endif + + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp new file mode 100644 index 0000000..e76f342 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp @@ -0,0 +1,117 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef GREENLET_CPYTHON_COMPAT_H +#define GREENLET_CPYTHON_COMPAT_H + +/** + * Helpers for compatibility with multiple versions of CPython. + */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + +/* +Python 3.10 beta 1 changed tstate->use_tracing to a nested cframe member. +See https://github.com/python/cpython/pull/25276 +We have to save and restore this as well. + +Python 3.13 removed PyThreadState.cframe (GH-108035). +*/ +#if PY_VERSION_HEX < 0x30D0000 +# define GREENLET_USE_CFRAME 1 +#else +# define GREENLET_USE_CFRAME 0 +#endif + + +#if PY_VERSION_HEX >= 0x30B00A4 +/* +Greenlet won't compile on anything older than Python 3.11 alpha 4 (see +https://bugs.python.org/issue46090). Summary of breaking internal changes: +- Python 3.11 alpha 1 changed how frame objects are represented internally. + - https://github.com/python/cpython/pull/30122 +- Python 3.11 alpha 3 changed how recursion limits are stored. + - https://github.com/python/cpython/pull/29524 +- Python 3.11 alpha 4 changed how exception state is stored. It also includes a + change to help greenlet save and restore the interpreter frame "data stack". + - https://github.com/python/cpython/pull/30122 + - https://github.com/python/cpython/pull/30234 +*/ +# define GREENLET_PY311 1 +#else +# define GREENLET_PY311 0 +#endif + + +#if PY_VERSION_HEX >= 0x30C0000 +# define GREENLET_PY312 1 +#else +# define GREENLET_PY312 0 +#endif + +#if PY_VERSION_HEX >= 0x30D0000 +# define GREENLET_PY313 1 +#else +# define GREENLET_PY313 0 +#endif + +#if PY_VERSION_HEX >= 0x30E0000 +# define GREENLET_PY314 1 +#else +# define GREENLET_PY314 0 +#endif + +#if PY_VERSION_HEX >= 0x30F0000 +# define GREENLET_PY315 1 +#else +# define GREENLET_PY315 0 +#endif + + +/* _Py_DEC_REFTOTAL macro has been removed from Python 3.9 by: + https://github.com/python/cpython/commit/49932fec62c616ec88da52642339d83ae719e924 + + The symbol we use to replace it was removed by at least 3.12. +*/ +#if defined(Py_REF_DEBUG) && !GREENLET_PY312 +# define GREENLET_Py_DEC_REFTOTAL _Py_RefTotal-- +#else +# define GREENLET_Py_DEC_REFTOTAL +#endif + + +#define G_TPFLAGS_DEFAULT Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_VERSION_TAG | Py_TPFLAGS_HAVE_GC + + +// bpo-43760 added PyThreadState_EnterTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_EnterTracing(PyThreadState *tstate) +{ + tstate->tracing++; + tstate->cframe->use_tracing = 0; +} +#endif + +// bpo-43760 added PyThreadState_LeaveTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_LeaveTracing(PyThreadState *tstate) +{ + tstate->tracing--; + int use_tracing = (tstate->c_tracefunc != NULL + || tstate->c_profilefunc != NULL); + tstate->cframe->use_tracing = use_tracing; +} +#endif + +#if !defined(Py_C_RECURSION_LIMIT) && defined(C_RECURSION_LIMIT) +# define Py_C_RECURSION_LIMIT C_RECURSION_LIMIT +#endif + +// Py_IsFinalizing() became a public API in Python 3.13. Map it to the +// private _Py_IsFinalizing() on older versions so all call sites can +// use the standard name. Remove this once greenlet drops support for +// Python < 3.13. +#if !GREENLET_PY313 +# define Py_IsFinalizing() _Py_IsFinalizing() +#endif + +#endif /* GREENLET_CPYTHON_COMPAT_H */ diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp new file mode 100644 index 0000000..617f07c --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp @@ -0,0 +1,171 @@ +#ifndef GREENLET_EXCEPTIONS_HPP +#define GREENLET_EXCEPTIONS_HPP + +#define PY_SSIZE_T_CLEAN +#include +#include +#include + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +#endif + +namespace greenlet { + + class PyErrOccurred : public std::runtime_error + { + public: + + // CAUTION: In debug builds, may run arbitrary Python code. + static const PyErrOccurred + from_current() + { + assert(PyErr_Occurred()); +#ifndef NDEBUG + // This is not exception safe, and + // not necessarily safe in general (what if it switches?) + // But we only do this in debug mode, where we are in + // tight control of what exceptions are getting raised and + // can prevent those issues. + + // You can't call PyObject_Str with a pending exception. + PyObject* typ; + PyObject* val; + PyObject* tb; + + PyErr_Fetch(&typ, &val, &tb); + PyObject* typs = PyObject_Str(typ); + PyObject* vals = PyObject_Str(val ? val : typ); + const char* typ_msg = PyUnicode_AsUTF8(typs); + const char* val_msg = PyUnicode_AsUTF8(vals); + PyErr_Restore(typ, val, tb); + + std::string msg(typ_msg); + msg += ": "; + msg += val_msg; + PyErrOccurred ex(msg); + Py_XDECREF(typs); + Py_XDECREF(vals); + + return ex; +#else + return PyErrOccurred(); +#endif + } + + PyErrOccurred() : std::runtime_error("") + { + assert(PyErr_Occurred()); + } + + PyErrOccurred(const std::string& msg) : std::runtime_error(msg) + { + assert(PyErr_Occurred()); + } + + PyErrOccurred(PyObject* exc_kind, const char* const msg) + : std::runtime_error(msg) + { + PyErr_SetString(exc_kind, msg); + } + + PyErrOccurred(PyObject* exc_kind, const std::string msg) + : std::runtime_error(msg) + { + // This copies the c_str, so we don't have any lifetime + // issues to worry about. + PyErr_SetString(exc_kind, msg.c_str()); + } + + PyErrOccurred(PyObject* exc_kind, + const std::string msg, //This is the format + //string; that's not + //usually safe! + + PyObject* borrowed_obj_one, PyObject* borrowed_obj_two) + : std::runtime_error(msg) + { + + //This is designed specifically for the + //``check_switch_allowed`` function. + + // PyObject_Str and PyObject_Repr are safe to call with + // NULL pointers; they return the string "" in that + // case. + // This function always returns null. + PyErr_Format(exc_kind, + msg.c_str(), + borrowed_obj_one, borrowed_obj_two); + } + }; + + class TypeError : public PyErrOccurred + { + public: + TypeError(const char* const what) + : PyErrOccurred(PyExc_TypeError, what) + { + } + TypeError(const std::string what) + : PyErrOccurred(PyExc_TypeError, what) + { + } + }; + + class ValueError : public PyErrOccurred + { + public: + ValueError(const char* const what) + : PyErrOccurred(PyExc_ValueError, what) + { + } + }; + + class AttributeError : public PyErrOccurred + { + public: + AttributeError(const char* const what) + : PyErrOccurred(PyExc_AttributeError, what) + { + } + }; + + /** + * Calls `Py_FatalError` when constructed, so you can't actually + * throw this. It just makes static analysis easier. + */ + class PyFatalError : public std::runtime_error + { + public: + PyFatalError(const char* const msg) + : std::runtime_error(msg) + { + Py_FatalError(msg); + } + }; + + static inline PyObject* + Require(PyObject* p, const std::string& msg="") + { + if (!p) { + throw PyErrOccurred(msg); + } + return p; + }; + + static inline void + Require(const int retval) + { + if (retval < 0) { + throw PyErrOccurred(); + } + }; + + +}; +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp new file mode 100644 index 0000000..4aca792 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp @@ -0,0 +1,109 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef GREENLET_INTERNAL_H +#define GREENLET_INTERNAL_H +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +#endif + +/** + * Implementation helpers. + * + * C++ templates and inline functions should go here. + */ +#define PY_SSIZE_T_CLEAN +#include "greenlet_compiler_compat.hpp" +#include "greenlet_cpython_compat.hpp" +#include "greenlet_exceptions.hpp" +#include "TGreenlet.hpp" +#include "greenlet_allocator.hpp" + +#include +#include + +#define GREENLET_MODULE +struct _greenlet; +typedef struct _greenlet PyGreenlet; +namespace greenlet { + + class ThreadState; + // We can't use the PythonAllocator for this, because we push to it + // from the thread state destructor, which doesn't have the GIL, + // and Python's allocators can only be called with the GIL. + typedef std::vector cleanup_queue_t; +}; + + +#define implementation_ptr_t greenlet::Greenlet* + + +#include "greenlet.h" + +void +greenlet::refs::MainGreenletExactChecker(void *p) +{ + if (!p) { + return; + } + if (greenlet::IsShuttingDown()) { + return; + } + // We control the class of the main greenlet exactly. + if (Py_TYPE(p) != &PyGreenlet_Type) { + std::string err("MainGreenlet: Expected exactly a greenlet, not a "); + err += Py_TYPE(p)->tp_name; + throw greenlet::TypeError(err); + } + + // Greenlets from dead threads no longer respond to main() with a + // true value; so in that case we need to perform an additional + // check. + Greenlet* g = static_cast(p)->pimpl; + if (g->main()) { + return; + } + if (!dynamic_cast(g)) { + std::string err("MainGreenlet: Expected exactly a main greenlet, not a "); + err += Py_TYPE(p)->tp_name; + throw greenlet::TypeError(err); + } +} + + + +template +inline greenlet::Greenlet* greenlet::refs::_OwnedGreenlet::operator->() const noexcept +{ + return reinterpret_cast(this->p)->pimpl; +} + +template +inline greenlet::Greenlet* greenlet::refs::_BorrowedGreenlet::operator->() const noexcept +{ + return reinterpret_cast(this->p)->pimpl; +} + +#include +#include + + +extern PyTypeObject PyGreenlet_Type; + + + +/** + * Forward declarations needed in multiple files. + */ +static PyObject* green_switch(PyGreenlet* self, PyObject* args, PyObject* kwargs); + + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + + +#endif + +// Local Variables: +// flycheck-clang-include-path: ("../../include" "/opt/local/Library/Frameworks/Python.framework/Versions/3.10/include/python3.10") +// End: diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp new file mode 100644 index 0000000..08fca47 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp @@ -0,0 +1,143 @@ +#ifndef GREENLET_MSVC_COMPAT_HPP +#define GREENLET_MSVC_COMPAT_HPP +/* + * Support for MSVC on Windows. + * + * Beginning with Python 3.14, some of the internal + * include files we need are not compatible with MSVC + * in C++ mode: + * + * internal\pycore_stackref.h(253): error C4576: a parenthesized type + * followed by an initializer list is a non-standard explicit type conversion syntax + * + * This file is included from ``internal/pycore_interpframe.h``, which + * we need for the ``_PyFrame_IsIncomplete`` API. + * + * Unfortunately, that API is a ``static inline`` function, as are a + * bunch of the functions it calls. The only solution seems to be to + * copy those definitions and the supporting inline functions here. + * + * Now, this makes us VERY fragile to changes in those functions. Because + * they're internal and static, the CPython devs might feel free to change + * them in even minor versions, meaning that we could runtime link and load, + * but still crash. We have that problem on all platforms though. It's just worse + * here because we have to keep copying the updated definitions. + */ +#include +#include "greenlet_cpython_compat.hpp" + +// This file is only included on 3.14+ + +extern "C" { + +// pycore_code.h ---------------- +#define _PyCode_CODE(CO) _Py_RVALUE((_Py_CODEUNIT *)(CO)->co_code_adaptive) + +#ifdef Py_GIL_DISABLED +static inline _PyCodeArray * +_PyCode_GetTLBCArray(PyCodeObject *co) +{ + return _Py_STATIC_CAST(_PyCodeArray *, + _Py_atomic_load_ptr_acquire(&co->co_tlbc)); +} +#endif +// End pycore_code.h ---------- + +// pycore_interpframe.h ---------- +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + +#define Py_TAG_BITS 0 +#else +#define Py_TAG_BITS ((uintptr_t)1) +#define Py_TAG_DEFERRED (1) +#define Py_INT_TAG 3 +#endif + + +static const _PyStackRef PyStackRef_NULL = { .bits = Py_TAG_DEFERRED}; +#define PyStackRef_IsNull(stackref) ((stackref).bits == PyStackRef_NULL.bits) + +static inline bool +PyStackRef_IsTaggedInt(_PyStackRef i) +{ + return (i.bits & Py_INT_TAG) == Py_INT_TAG; +} + +static inline bool +PyStackRef_IsNullOrInt(_PyStackRef stackref) +{ + return PyStackRef_IsNull(stackref) || PyStackRef_IsTaggedInt(stackref); +} + +#define _Py_VISIT_STACKREF(ref) \ + do { \ + if (!PyStackRef_IsNullOrInt(ref)) { \ + int vret = _PyGC_VisitStackRef(&(ref), visit, arg); \ + if (vret) \ + return vret; \ + } \ + } while (0) + +static inline PyObject * +PyStackRef_AsPyObjectBorrow(_PyStackRef stackref) +{ + PyObject *cleared = ((PyObject *)((stackref).bits & (~Py_TAG_BITS))); + return cleared; +} + +#define Py_TAG_REFCNT 1 +#define BITS_TO_PTR(ref) ((PyObject *)((ref).bits)) + +#define PyStackRef_RefcountOnObject(ref) (((ref).bits & Py_TAG_REFCNT) == 0) + +#define PyStackRef_CLOSE(REF) \ + do { \ + _PyStackRef _close_tmp = (REF); \ + if (PyStackRef_RefcountOnObject(_close_tmp)) { \ + Py_DECREF(BITS_TO_PTR(_close_tmp)); \ + } \ + } while (0) + +#define PyStackRef_CLEAR(REF) \ + do { \ + _PyStackRef* _clear_ptr = &(REF); \ + _PyStackRef _clear_old = (*_clear_ptr); \ + *_clear_ptr = PyStackRef_NULL; \ + PyStackRef_CLOSE(_clear_old); \ + } while (0) + +static inline PyCodeObject *_PyFrame_GetCode(_PyInterpreterFrame *f) { + assert(!PyStackRef_IsNullOrInt(f->f_executable)); + PyObject *executable = PyStackRef_AsPyObjectBorrow(f->f_executable); + assert(PyCode_Check(executable)); + return (PyCodeObject *)executable; +} + + +static inline _Py_CODEUNIT * +_PyFrame_GetBytecode(_PyInterpreterFrame *f) +{ +#ifdef Py_GIL_DISABLED + PyCodeObject *co = _PyFrame_GetCode(f); + _PyCodeArray *tlbc = _PyCode_GetTLBCArray(co); + assert(f->tlbc_index >= 0 && f->tlbc_index < tlbc->size); + return (_Py_CODEUNIT *)tlbc->entries[f->tlbc_index]; +#else + return _PyCode_CODE(_PyFrame_GetCode(f)); +#endif +} + +static inline bool //_Py_NO_SANITIZE_THREAD +_PyFrame_IsIncomplete(_PyInterpreterFrame *frame) +{ + if (frame->owner >= FRAME_OWNED_BY_INTERPRETER) { + return true; + } + return frame->owner != FRAME_OWNED_BY_GENERATOR && + frame->instr_ptr < _PyFrame_GetBytecode(frame) + + _PyFrame_GetCode(frame)->_co_firsttraceable; +} +// pycore_interpframe.h ---------- + +} +#endif // GREENLET_MSVC_COMPAT_HPP diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp new file mode 100644 index 0000000..9b936d6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp @@ -0,0 +1,1199 @@ +#ifndef GREENLET_REFS_HPP +#define GREENLET_REFS_HPP + +#define PY_SSIZE_T_CLEAN +#include + +#include + +//#include "greenlet_internal.hpp" +#include "greenlet_compiler_compat.hpp" +#include "greenlet_cpython_compat.hpp" +#include "greenlet_exceptions.hpp" + +struct _greenlet; +struct _PyMainGreenlet; + +typedef struct _greenlet PyGreenlet; +extern PyTypeObject PyGreenlet_Type; + + +#ifdef GREENLET_USE_STDIO +#include +using std::cerr; +using std::endl; +#endif + +namespace greenlet +{ + class Greenlet; + + static inline bool + IsShuttingDown() + { + // This used to check a flag set by an ``atexit`` callback. + // This was wrong: the interpreter is still fully functional + // while *all* atexit callbacks are run, and it is perfectly + // valid for an atexit callback that runs after our atexit + // callback (i.e., registered first/before ours) to want to + // make use of greenlet services --- this comes up easily with + // gevent monkey-patching. Almost immediately after atexit callbacks, + // and before any destructive action is taken, Python arranges + // for Py_IsFinalizing to become true. + + // It may see me could potentially tighten this check even more (and + // eliminate a function call) by setting a flag in a + // destructor function for our PyCapsule object (_C_API) to + // determine when we're shutting down. ``Py_IsFinalizing`` + // becomes true relatively early in the shutdown process, + // while Capsule destructor functions only run when the module + // has actually been torn down --- well, when all of its dicts are + // cleared and collected; recall that because we use + // single-phase init, there is a "hidden" copy of the module + // dict kept by CPython internals used to re-populate a module + // if greenlet is imported twice, so Python code can't trigger + // C_API to get GC'd early without seriously poking at CPython + // internals, e.g., by using `gc.get_referrers` to find the + // hidden dict. However, C extensions could have INCREF the + // capsule object and prevent it from *ever* getting torn + // down, so this isn't reliable. + + // We could probably be even "smarter" and replace values in + // _PyGreenlet_API with different values at destruction time. + // For the PyObject* returning APIs, we could replace them + // with versions that set an exception and return null --- the + // benefit being that we don't have to include a + // Py_IsFinalizing() call in the normal path; int returning + // APIs would be handled on a case-by-case basis; unclear what + // to do with the types. This is of questionable benefit + // though because by the time our destructor is called, our + // module is about to be destroyed which may take our + // allocated storage with it (if CPython ever dynamically + // unloads loaded shared libraries, which as of 3.14 it never + // does). + + return Py_IsFinalizing(); + } + + namespace refs + { + // Type checkers throw a TypeError if the argument is not + // null, and isn't of the required Python type. + // (We can't use most of the defined type checkers + // like PyList_Check, etc, directly, because they are + // implemented as macros.) + typedef void (*TypeChecker)(void*); + + void + NoOpChecker(void*) + { + return; + } + + void + GreenletChecker(void *p) + { + if (!p) { + return; + } + if (IsShuttingDown()) { + return; + } + + PyTypeObject* typ = Py_TYPE(p); + // fast, common path. (PyObject_TypeCheck is a macro or + // static inline function, and it also does a + // direct comparison of the type pointers, but its fast + // path only handles one type) + if (typ == &PyGreenlet_Type) { + return; + } + + if (!PyObject_TypeCheck(p, &PyGreenlet_Type)) { + std::string err("GreenletChecker: Expected any type of greenlet, not "); + err += Py_TYPE(p)->tp_name; + throw TypeError(err); + } + } + + void + MainGreenletExactChecker(void *p); + + template + class PyObjectPointer; + + template + class OwnedReference; + + + template + class BorrowedReference; + + typedef BorrowedReference BorrowedObject; + typedef OwnedReference OwnedObject; + + class ImmortalObject; + class ImmortalString; + + template + class _OwnedGreenlet; + + typedef _OwnedGreenlet OwnedGreenlet; + typedef _OwnedGreenlet OwnedMainGreenlet; + + template + class _BorrowedGreenlet; + + typedef _BorrowedGreenlet BorrowedGreenlet; + + void + ContextExactChecker(void *p) + { + if (!p) { + return; + } + if (IsShuttingDown()) { + return; + } + if (!PyContext_CheckExact(p)) { + throw TypeError( + "greenlet context must be a contextvars.Context or None" + ); + } + } + + typedef OwnedReference OwnedContext; + } +} + +namespace greenlet { + + + namespace refs { + // A set of classes to make reference counting rules in python + // code explicit. + // + // Rules of use: + // (1) Functions returning a new reference that the caller of the + // function is expected to dispose of should return a + // ``OwnedObject`` object. This object automatically releases its + // reference when it goes out of scope. It works like a ``std::shared_ptr`` + // and can be copied or used as a function parameter (but don't do + // that). Note that constructing a ``OwnedObject`` from a + // PyObject* steals the reference. + // (2) Parameters to functions should be either a + // ``OwnedObject&``, or, more generally, a ``PyObjectPointer&``. + // If the function needs to create its own new reference, it can + // do so by copying to a local ``OwnedObject``. + // (3) Functions returning an existing pointer that is NOT + // incref'd, and which the caller MUST NOT decref, + // should return a ``BorrowedObject``. + + // XXX: The following two paragraphs do not hold for all platforms. + // Notably, 32-bit PPC Linux passes structs by reference, not by + // value, so this actually doesn't work. (Although that's the only + // platform that doesn't work on.) DO NOT ATTEMPT IT. The + // unfortunate consequence of that is that the slots which we + // *know* are already type safe will wind up calling the type + // checker function (when we had the slots accepting + // BorrowedGreenlet, this was bypassed), so this slows us down. + // TODO: Optimize this again. + + // For a class with a single pointer member, whose constructor + // does nothing but copy a pointer parameter into the member, and + // which can then be converted back to the pointer type, compilers + // generate code that's the same as just passing the pointer. + // That is, func(BorrowedObject x) called like ``PyObject* p = + // ...; f(p)`` has 0 overhead. Similarly, they "unpack" to the + // pointer type with 0 overhead. + // + // If there are no virtual functions, no complex inheritance (maybe?) and + // no destructor, these can be directly used as parameters in + // Python callbacks like tp_init: the layout is the same as a + // single pointer. Only subclasses with trivial constructors that + // do nothing but set the single pointer member are safe to use + // that way. + + + // This is the base class for things that can be done with a + // PyObject pointer. It assumes nothing about memory management. + // NOTE: Nothing is virtual, so subclasses shouldn't add new + // storage fields or try to override these methods. + template + class PyObjectPointer + { + public: + typedef T PyType; + protected: + T* p; + public: + PyObjectPointer(T* it=nullptr) : p(it) + { + TC(p); + } + + // We don't allow automatic casting to PyObject* at this + // level, because then we could be passed to Py_DECREF/INCREF, + // but we want nothing to do with memory management. If you + // know better, then you can use the get() method, like on a + // std::shared_ptr. Except we name it borrow() to clarify that + // if this is a reference-tracked object, the pointer you get + // back will go away when the object does. + // TODO: This should probably not exist here, but be moved + // down to relevant sub-types. + + T* borrow() const noexcept + { + return this->p; + } + + PyObject* borrow_o() const noexcept + { + return reinterpret_cast(this->p); + } + + T* operator->() const noexcept + { + return this->p; + } + + bool is_None() const noexcept + { + return this->p == Py_None; + } + + PyObject* acquire_or_None() const noexcept + { + PyObject* result = this->p ? reinterpret_cast(this->p) : Py_None; + Py_INCREF(result); + return result; + } + + explicit operator bool() const noexcept + { + return this->p != nullptr; + } + + bool operator!() const noexcept + { + return this->p == nullptr; + } + + Py_ssize_t REFCNT() const noexcept + { + return p ? Py_REFCNT(p) : -42; + } + + PyTypeObject* TYPE() const noexcept + { + return p ? Py_TYPE(p) : nullptr; + } + + inline OwnedObject PyStr() const noexcept; + inline const std::string as_str() const noexcept; + inline OwnedObject PyGetAttr(const ImmortalObject& name) const noexcept; + inline OwnedObject PyRequireAttr(const char* const name) const; + inline OwnedObject PyRequireAttr(const ImmortalString& name) const; + inline OwnedObject PyCall(const BorrowedObject& arg) const; + inline OwnedObject PyCall(PyGreenlet* arg) const ; + inline OwnedObject PyCall(PyObject* arg) const ; + // PyObject_Call(this, args, kwargs); + inline OwnedObject PyCall(const BorrowedObject args, + const BorrowedObject kwargs) const; + inline OwnedObject PyCall(const OwnedObject& args, + const OwnedObject& kwargs) const; + + protected: + void _set_raw_pointer(void* t) + { + TC(t); + p = reinterpret_cast(t); + } + void* _get_raw_pointer() const + { + return p; + } + }; + +#ifdef GREENLET_USE_STDIO + template + std::ostream& operator<<(std::ostream& os, const PyObjectPointer& s) + { + const std::type_info& t = typeid(s); + os << t.name() + << "(addr=" << s.borrow() + << ", refcnt=" << s.REFCNT() + << ", value=" << s.as_str() + << ")"; + + return os; + } +#endif + + template + inline bool operator==(const PyObjectPointer& lhs, const PyObject* const rhs) noexcept + { + return static_cast(lhs.borrow_o()) == static_cast(rhs); + } + + template + inline bool operator==(const PyObjectPointer& lhs, const PyObjectPointer& rhs) noexcept + { + return lhs.borrow_o() == rhs.borrow_o(); + } + + template + inline bool operator!=(const PyObjectPointer& lhs, + const PyObjectPointer& rhs) noexcept + { + return lhs.borrow_o() != rhs.borrow_o(); + } + + template + class OwnedReference : public PyObjectPointer + { + private: + friend class OwnedList; + + protected: + explicit OwnedReference(T* it) : PyObjectPointer(it) + { + } + + public: + + // Constructors + + static OwnedReference consuming(PyObject* p) + { + return OwnedReference(reinterpret_cast(p)); + } + + static OwnedReference owning(T* p) + { + OwnedReference result(p); + Py_XINCREF(result.p); + return result; + } + + OwnedReference() : PyObjectPointer(nullptr) + {} + + explicit OwnedReference(const PyObjectPointer<>& other) + : PyObjectPointer(nullptr) + { + T* op = other.borrow(); + TC(op); + this->p = other.borrow(); + Py_XINCREF(this->p); + } + + // It would be good to make use of the C++11 distinction + // between move and copy operations, e.g., constructing from a + // pointer should be a move operation. + // In the common case of ``OwnedObject x = Py_SomeFunction()``, + // the call to the copy constructor will be elided completely. + OwnedReference(const OwnedReference& other) + : PyObjectPointer(other.p) + { + Py_XINCREF(this->p); + } + + static OwnedReference None() + { + Py_INCREF(Py_None); + return OwnedReference(Py_None); + } + + // We can assign from exactly our type without any extra checking + OwnedReference& operator=(const OwnedReference& other) + { + Py_XINCREF(other.p); + const T* tmp = this->p; + this->p = other.p; + Py_XDECREF(tmp); + return *this; + } + + OwnedReference& operator=(const BorrowedReference other) + { + return this->operator=(other.borrow()); + } + + OwnedReference& operator=(T* const other) + { + TC(other); + Py_XINCREF(other); + T* tmp = this->p; + this->p = other; + Py_XDECREF(tmp); + return *this; + } + + // We can assign from an arbitrary reference type + // if it passes our check. + template + OwnedReference& operator=(const OwnedReference& other) + { + X* op = other.borrow(); + TC(op); + return this->operator=(reinterpret_cast(op)); + } + + inline void steal(T* other) + { + assert(this->p == nullptr); + TC(other); + this->p = other; + } + + T* relinquish_ownership() + { + T* result = this->p; + this->p = nullptr; + return result; + } + + T* acquire() const + { + // Return a new reference. + // TODO: This may go away when we have reference objects + // throughout the code. + Py_XINCREF(this->p); + return this->p; + } + + // Nothing else declares a destructor, we're the leaf, so we + // should be able to get away without virtual. + ~OwnedReference() + { + Py_CLEAR(this->p); + } + + void CLEAR() + { + Py_CLEAR(this->p); + assert(this->p == nullptr); + } + }; + + static inline + void operator<<=(PyObject*& target, OwnedObject& o) + { + target = o.relinquish_ownership(); + } + + + class NewReference : public OwnedObject + { + private: + G_NO_COPIES_OF_CLS(NewReference); + public: + // Consumes the reference. Only use this + // for API return values. + NewReference(PyObject* it) : OwnedObject(it) + { + } + }; + + class NewDictReference : public NewReference + { + private: + G_NO_COPIES_OF_CLS(NewDictReference); + public: + NewDictReference() : NewReference(PyDict_New()) + { + if (!this->p) { + throw PyErrOccurred(); + } + } + + void SetItem(const char* const key, PyObject* value) + { + Require(PyDict_SetItemString(this->p, key, value)); + } + + void SetItem(const PyObjectPointer<>& key, PyObject* value) + { + Require(PyDict_SetItem(this->p, key.borrow_o(), value)); + } + }; + + template + class _OwnedGreenlet: public OwnedReference + { + private: + protected: + _OwnedGreenlet(T* it) : OwnedReference(it) + {} + + public: + _OwnedGreenlet() : OwnedReference() + {} + + _OwnedGreenlet(const _OwnedGreenlet& other) : OwnedReference(other) + { + } + _OwnedGreenlet(OwnedMainGreenlet& other) : + OwnedReference(reinterpret_cast(other.acquire())) + { + } + _OwnedGreenlet(const BorrowedGreenlet& other); + // Steals a reference. + static _OwnedGreenlet consuming(PyGreenlet* it) + { + return _OwnedGreenlet(reinterpret_cast(it)); + } + + inline _OwnedGreenlet& operator=(const OwnedGreenlet& other) + { + return this->operator=(other.borrow()); + } + + inline _OwnedGreenlet& operator=(const BorrowedGreenlet& other); + + _OwnedGreenlet& operator=(const OwnedMainGreenlet& other) + { + PyGreenlet* owned = other.acquire(); + Py_XDECREF(this->p); + this->p = reinterpret_cast(owned); + return *this; + } + + _OwnedGreenlet& operator=(T* const other) + { + OwnedReference::operator=(other); + return *this; + } + + T* relinquish_ownership() + { + T* result = this->p; + this->p = nullptr; + return result; + } + + PyObject* relinquish_ownership_o() + { + return reinterpret_cast(relinquish_ownership()); + } + + inline Greenlet* operator->() const noexcept; + inline operator Greenlet*() const noexcept; + }; + + template + class BorrowedReference : public PyObjectPointer + { + public: + // Allow implicit creation from PyObject* pointers as we + // transition to using these classes. Also allow automatic + // conversion to PyObject* for passing to C API calls and even + // for Py_INCREF/DECREF, because we ourselves do no memory management. + BorrowedReference(T* it) : PyObjectPointer(it) + {} + + BorrowedReference(const PyObjectPointer& ref) : PyObjectPointer(ref.borrow()) + {} + + BorrowedReference() : PyObjectPointer(nullptr) + {} + + operator T*() const + { + return this->p; + } + }; + + typedef BorrowedReference BorrowedObject; + //typedef BorrowedReference BorrowedGreenlet; + + template + class _BorrowedGreenlet : public BorrowedReference + { + public: + _BorrowedGreenlet() : + BorrowedReference(nullptr) + {} + + _BorrowedGreenlet(T* it) : + BorrowedReference(it) + {} + + _BorrowedGreenlet(const BorrowedObject& it); + + _BorrowedGreenlet(const OwnedGreenlet& it) : + BorrowedReference(it.borrow()) + {} + + _BorrowedGreenlet& operator=(const BorrowedObject& other); + + // We get one of these for PyGreenlet, but one for PyObject + // is handy as well + operator PyObject*() const + { + return reinterpret_cast(this->p); + } + Greenlet* operator->() const noexcept; + operator Greenlet*() const noexcept; + }; + + typedef _BorrowedGreenlet BorrowedGreenlet; + + template + _OwnedGreenlet::_OwnedGreenlet(const BorrowedGreenlet& other) + : OwnedReference(reinterpret_cast(other.borrow())) + { + Py_XINCREF(this->p); + } + + + class BorrowedMainGreenlet + : public _BorrowedGreenlet + { + public: + BorrowedMainGreenlet(const OwnedMainGreenlet& it) : + _BorrowedGreenlet(it.borrow()) + {} + BorrowedMainGreenlet(PyGreenlet* it=nullptr) + : _BorrowedGreenlet(it) + {} + }; + + template + _OwnedGreenlet& _OwnedGreenlet::operator=(const BorrowedGreenlet& other) + { + return this->operator=(other.borrow()); + } + + + class ImmortalObject : public PyObjectPointer<> + { + private: + G_NO_ASSIGNMENT_OF_CLS(ImmortalObject); + public: + explicit ImmortalObject(PyObject* it) : PyObjectPointer<>(it) + { + } + + ImmortalObject(const ImmortalObject& other) + : PyObjectPointer<>(other.p) + { + + } + + /** + * Become the new owner of the object. Does not change the + * reference count. + */ + ImmortalObject& operator=(PyObject* it) + { + assert(this->p == nullptr); + this->p = it; + return *this; + } + + static ImmortalObject consuming(PyObject* it) + { + return ImmortalObject(it); + } + + inline operator PyObject*() const + { + return this->p; + } + }; + + class ImmortalString : public ImmortalObject + { + private: + G_NO_COPIES_OF_CLS(ImmortalString); + const char* str; + public: + ImmortalString(const char* const str) : + ImmortalObject(str ? Require(PyUnicode_InternFromString(str)) : nullptr) + { + this->str = str; + } + + inline ImmortalString& operator=(const char* const str) + { + if (!this->p) { + this->p = Require(PyUnicode_InternFromString(str)); + this->str = str; + } + else { + assert(this->str == str); + } + return *this; + } + + inline operator std::string() const + { + return this->str; + } + + }; + + class ImmortalEventName : public ImmortalString + { + private: + G_NO_COPIES_OF_CLS(ImmortalEventName); + public: + ImmortalEventName(const char* const str) : ImmortalString(str) + {} + }; + + class ImmortalException : public ImmortalObject + { + private: + G_NO_COPIES_OF_CLS(ImmortalException); + public: + ImmortalException(const char* const name, PyObject* base=nullptr) : + ImmortalObject(name + // Python 2.7 isn't const correct + ? Require(PyErr_NewException((char*)name, base, nullptr)) + : nullptr) + {} + + inline bool PyExceptionMatches() const + { + return PyErr_ExceptionMatches(this->p) > 0; + } + + }; + + template + inline OwnedObject PyObjectPointer::PyStr() const noexcept + { + if (!this->p) { + return OwnedObject(); + } + return OwnedObject::consuming(PyObject_Str(reinterpret_cast(this->p))); + } + + template + inline const std::string PyObjectPointer::as_str() const noexcept + { + // NOTE: This is not Python exception safe. + if (this->p) { + // The Python APIs return a cached char* value that's only valid + // as long as the original object stays around, and we're + // about to (probably) toss it. Hence the copy to std::string. + OwnedObject py_str = this->PyStr(); + if (!py_str) { + return "(nil)"; + } + return PyUnicode_AsUTF8(py_str.borrow()); + } + return "(nil)"; + } + + template + inline OwnedObject PyObjectPointer::PyGetAttr(const ImmortalObject& name) const noexcept + { + assert(this->p); + return OwnedObject::consuming(PyObject_GetAttr(reinterpret_cast(this->p), name)); + } + + template + inline OwnedObject PyObjectPointer::PyRequireAttr(const char* const name) const + { + assert(this->p); + return OwnedObject::consuming(Require(PyObject_GetAttrString(this->p, name), name)); + } + + template + inline OwnedObject PyObjectPointer::PyRequireAttr(const ImmortalString& name) const + { + assert(this->p); + return OwnedObject::consuming(Require( + PyObject_GetAttr( + reinterpret_cast(this->p), + name + ), + name + )); + } + + template + inline OwnedObject PyObjectPointer::PyCall(const BorrowedObject& arg) const + { + return this->PyCall(arg.borrow()); + } + + template + inline OwnedObject PyObjectPointer::PyCall(PyGreenlet* arg) const + { + return this->PyCall(reinterpret_cast(arg)); + } + + template + inline OwnedObject PyObjectPointer::PyCall(PyObject* arg) const + { + assert(this->p); + return OwnedObject::consuming(PyObject_CallFunctionObjArgs(this->p, arg, NULL)); + } + + template + inline OwnedObject PyObjectPointer::PyCall(const BorrowedObject args, + const BorrowedObject kwargs) const + { + assert(this->p); + return OwnedObject::consuming(PyObject_Call(this->p, args, kwargs)); + } + + template + inline OwnedObject PyObjectPointer::PyCall(const OwnedObject& args, + const OwnedObject& kwargs) const + { + assert(this->p); + return OwnedObject::consuming(PyObject_Call(this->p, args.borrow(), kwargs.borrow())); + } + + inline void + ListChecker(void * p) + { + if (!p) { + return; + } + if (!PyList_Check(p)) { + throw TypeError("Expected a list"); + } + } + + class OwnedList : public OwnedReference + { + private: + G_NO_ASSIGNMENT_OF_CLS(OwnedList); + public: + // TODO: Would like to use move. + explicit OwnedList(const OwnedObject& other) + : OwnedReference(other) + { + } + + OwnedList& operator=(const OwnedObject& other) + { + if (other && PyList_Check(other.p)) { + // Valid list. Own a new reference to it, discard the + // reference to what we did own. + PyObject* new_ptr = other.p; + Py_INCREF(new_ptr); + Py_XDECREF(this->p); + this->p = new_ptr; + } + else { + // Either the other object was NULL (an error) or it + // wasn't a list. Either way, we're now invalidated. + Py_XDECREF(this->p); + this->p = nullptr; + } + return *this; + } + + inline bool empty() const + { + return PyList_GET_SIZE(p) == 0; + } + + inline Py_ssize_t size() const + { + return PyList_GET_SIZE(p); + } + + inline BorrowedObject at(const Py_ssize_t index) const + { + return PyList_GET_ITEM(p, index); + } + + inline void clear() + { + PyList_SetSlice(p, 0, PyList_GET_SIZE(p), NULL); + } + }; + + // Use this to represent the module object used at module init + // time. + // This could either be a borrowed (Py2) or new (Py3) reference; + // either way, we don't want to do any memory management + // on it here, Python itself will handle that. + // XXX: Actually, that's not quite right. On Python 3, if an + // exception occurs before we return to the interpreter, this will + // leak; but all previous versions also had that problem. + class CreatedModule : public PyObjectPointer<> + { + private: + G_NO_COPIES_OF_CLS(CreatedModule); + public: + CreatedModule(PyModuleDef& mod_def) : PyObjectPointer<>( + Require(PyModule_Create(&mod_def))) + { + } + + // PyAddObject(): Add a new reference to the object to the module. + void PyAddObject(const char* name, const long new_bool) + { + Require(PyModule_AddIntConstant(this->p, name, new_bool)); + } + + // It is safe to pass a null value to this API because we use + // PyModule_AddObjectRef under the covers which allows null. + void PyAddObject(const char* name, const OwnedObject& new_object) + { + // The caller already owns a reference they will decref + // when their variable goes out of scope, we still need to + // incref/decref. + this->PyAddObject(name, new_object.borrow()); + } + + void PyAddObject(const char* name, const ImmortalObject& new_object) + { + this->PyAddObject(name, new_object.borrow()); + } + + void PyAddObject(const char* name, PyTypeObject& type) + { + this->PyAddObject(name, reinterpret_cast(&type)); + } + + private: + + void PyAddObject(const char* name, PyObject* new_object) + { + Require(PyModule_AddObjectRef(this->p, name, new_object)); + } + }; + + class PyErrFetchParam : public PyObjectPointer<> + { + // Not an owned object, because we can't be initialized with + // one, and we only sometimes acquire ownership. + private: + G_NO_COPIES_OF_CLS(PyErrFetchParam); + public: + // To allow declaring these and passing them to + // PyErr_Fetch we implement the empty constructor, + // and the address operator. + PyErrFetchParam() : PyObjectPointer<>(nullptr) + { + } + + PyObject** operator&() + { + return &this->p; + } + + // This allows us to pass one directly without the &, + // BUT it has higher precedence than the bool operator + // if it's not explicit. + operator PyObject**() + { + return &this->p; + } + + // We don't want to be able to pass these to Py_DECREF and + // such so we don't have the implicit PyObject* conversion. + + inline PyObject* relinquish_ownership() + { + PyObject* result = this->p; + this->p = nullptr; + return result; + } + + ~PyErrFetchParam() + { + Py_XDECREF(p); + } + }; + + class OwnedErrPiece : public OwnedObject + { + private: + + public: + // Unlike OwnedObject, this increments the refcount. + OwnedErrPiece(PyObject* p=nullptr) : OwnedObject(p) + { + this->acquire(); + } + + PyObject** operator&() + { + return &this->p; + } + + inline operator PyObject*() const + { + return this->p; + } + + operator PyTypeObject*() const + { + return reinterpret_cast(this->p); + } + }; + + // TODO: When we run on 3.12+ only (GREENLET_312), switch to the + // ``PyErr_GetRaisedException`` family of functions. The + // ``PyErr_Fetch`` family is deprecated on 3.12+, but is part + // of the stable ABI so it's not going anywhere. + class PyErrPieces + { + private: + OwnedErrPiece type; + OwnedErrPiece instance; + OwnedErrPiece traceback; + bool restored; + public: + // Takes new references; if we're destroyed before + // restoring the error, we drop the references. + PyErrPieces(PyObject* t, PyObject* v, PyObject* tb) : + type(t), + instance(v), + traceback(tb), + restored(0) + { + this->normalize(); + } + + PyErrPieces() : + restored(0) + { + // PyErr_Fetch transfers ownership to us, so + // we don't actually need to INCREF; but we *do* + // need to DECREF if we're not restored. + PyErrFetchParam t, v, tb; + PyErr_Fetch(&t, &v, &tb); + type.steal(t.relinquish_ownership()); + instance.steal(v.relinquish_ownership()); + traceback.steal(tb.relinquish_ownership()); + } + + void PyErrRestore() + { + // can only do this once + assert(!this->restored); + this->restored = true; + PyErr_Restore( + this->type.relinquish_ownership(), + this->instance.relinquish_ownership(), + this->traceback.relinquish_ownership()); + assert(!this->type && !this->instance && !this->traceback); + } + + private: + void normalize() + { + // First, check the traceback argument, replacing None, + // with NULL + if (traceback.is_None()) { + traceback = nullptr; + } + + if (traceback && !PyTraceBack_Check(traceback.borrow())) { + throw PyErrOccurred(PyExc_TypeError, + "throw() third argument must be a traceback object"); + } + + if (PyExceptionClass_Check(type)) { + // If we just had a type, we'll now have a type and + // instance. + // The type's refcount will have gone up by one + // because of the instance and the instance will have + // a refcount of one. Either way, we owned, and still + // do own, exactly one reference. + PyErr_NormalizeException(&type, &instance, &traceback); + + } + else if (PyExceptionInstance_Check(type)) { + /* Raising an instance --- usually that means an + object that is a subclass of BaseException, but on + Python 2, that can also mean an arbitrary old-style + object. The value should be a dummy. */ + if (instance && !instance.is_None()) { + throw PyErrOccurred( + PyExc_TypeError, + "instance exception may not have a separate value"); + } + /* Normalize to raise , */ + this->instance = this->type; + this->type = PyExceptionInstance_Class(instance.borrow()); + + /* + It would be tempting to do this: + + Py_ssize_t type_count = Py_REFCNT(Py_TYPE(instance.borrow())); + this->type = PyExceptionInstance_Class(instance.borrow()); + assert(this->type.REFCNT() == type_count + 1); + + But that doesn't work on Python 2 in the case of + old-style instances: The result of Py_TYPE is going to + be the global shared that all + old-style classes have, while the return of Instance_Class() + will be the Python-level class object. The two are unrelated. + */ + } + else { + /* Not something you can raise. throw() fails. */ + PyErr_Format(PyExc_TypeError, + "exceptions must be classes, or instances, not %s", + Py_TYPE(type.borrow())->tp_name); + throw PyErrOccurred(); + } + } + }; + + // PyArg_Parse's O argument returns a borrowed reference. + class PyArgParseParam : public BorrowedObject + { + private: + G_NO_COPIES_OF_CLS(PyArgParseParam); + public: + explicit PyArgParseParam(PyObject* p=nullptr) : BorrowedObject(p) + { + } + + inline PyObject** operator&() + { + return &this->p; + } + }; + +#ifdef Py_GIL_DISABLED + // building on 3.13 or newer, free-threaded + class PyCriticalObjectSection { + private: + G_NO_COPIES_OF_CLS(PyCriticalObjectSection); + PyCriticalSection _py_cs; + public: + explicit PyCriticalObjectSection(PyObject* p) + { + PyCriticalSection_Begin(&this->_py_cs, p); + } + explicit PyCriticalObjectSection(const PyGreenlet* p) + : PyCriticalObjectSection( + reinterpret_cast( + const_cast(p))) + {} + ~PyCriticalObjectSection() + { + PyCriticalSection_End(&this->_py_cs); + } + }; +#else + class PyCriticalObjectSection { + public: + explicit PyCriticalObjectSection(PyObject* UNUSED(p)) + {} + explicit PyCriticalObjectSection(const PyGreenlet* UNUSED(p)) + {} + }; + +#endif + + +};}; + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp new file mode 100644 index 0000000..bdffcca --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp @@ -0,0 +1,103 @@ +#ifndef GREENLET_SLP_SWITCH_HPP +#define GREENLET_SLP_SWITCH_HPP + +#include "greenlet_compiler_compat.hpp" +#include "greenlet_refs.hpp" + +/* + * the following macros are spliced into the OS/compiler + * specific code, in order to simplify maintenance. + */ +// We can save about 10% of the time it takes to switch greenlets if +// we thread the thread state through the slp_save_state() and the +// following slp_restore_state() calls from +// slp_switch()->g_switchstack() (which already needs to access it). +// +// However: +// +// that requires changing the prototypes and implementations of the +// switching functions. If we just change the prototype of +// slp_switch() to accept the argument and update the macros, without +// changing the implementation of slp_switch(), we get crashes on +// 64-bit Linux and 32-bit x86 (for reasons that aren't 100% clear); +// on the other hand, 64-bit macOS seems to be fine. Also, 64-bit +// windows is an issue because slp_switch is written fully in assembly +// and currently ignores its argument so some code would have to be +// adjusted there to pass the argument on to the +// ``slp_save_state_asm()`` function (but interestingly, because of +// the calling convention, the extra argument is just ignored and +// things function fine, albeit slower, if we just modify +// ``slp_save_state_asm`()` to fetch the pointer to pass to the +// macro.) +// +// Our compromise is to use a *glabal*, untracked, weak, pointer +// to the necessary thread state during the process of switching only. +// This is safe because we're protected by the GIL, and if we're +// running this code, the thread isn't exiting. This also nets us a +// 10-12% speed improvement. + +#if Py_GIL_DISABLED +thread_local greenlet::Greenlet* switching_thread_state = nullptr; +#else +static greenlet::Greenlet* volatile switching_thread_state = nullptr; +#endif + + +extern "C" { +static int GREENLET_NOINLINE(slp_save_state_trampoline)(char* stackref); +static void GREENLET_NOINLINE(slp_restore_state_trampoline)(); +} + + +#define SLP_SAVE_STATE(stackref, stsizediff) \ +do { \ + assert(switching_thread_state); \ + stackref += STACK_MAGIC; \ + if (slp_save_state_trampoline((char*)stackref)) \ + return -1; \ + if (!switching_thread_state->active()) \ + return 1; \ + stsizediff = switching_thread_state->stack_start() - (char*)stackref; \ +} while (0) + +#define SLP_RESTORE_STATE() slp_restore_state_trampoline() + +#define SLP_EVAL +extern "C" { +#define slp_switch GREENLET_NOINLINE(slp_switch) +#include "slp_platformselect.h" +} +#undef slp_switch + +#ifndef STACK_MAGIC +# error \ + "greenlet needs to be ported to this platform, or taught how to detect your compiler properly." +#endif /* !STACK_MAGIC */ + + + +#ifdef EXTERNAL_ASM +/* CCP addition: Make these functions, to be called from assembler. + * The token include file for the given platform should enable the + * EXTERNAL_ASM define so that this is included. + */ +extern "C" { +intptr_t +slp_save_state_asm(intptr_t* ref) +{ + intptr_t diff; + SLP_SAVE_STATE(ref, diff); + return diff; +} + +void +slp_restore_state_asm(void) +{ + SLP_RESTORE_STATE(); +} + +extern int slp_switch(void); +}; +#endif + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp b/venv/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp new file mode 100644 index 0000000..3ded7d2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp @@ -0,0 +1,31 @@ +#ifndef GREENLET_THREAD_SUPPORT_HPP +#define GREENLET_THREAD_SUPPORT_HPP + +/** + * Defines various utility functions to help greenlet integrate well + * with threads. This used to be needed when we supported Python + * 2.7 on Windows, which used a very old compiler. We wrote an + * alternative implementation using Python APIs and POSIX or Windows + * APIs, but that's no longer needed. So this file is a shadow of its + * former self --- but may be needed in the future. + */ + +#include +#include +#include + +#include "greenlet_compiler_compat.hpp" + +namespace greenlet { + typedef std::mutex Mutex; + typedef std::lock_guard LockGuard; + class LockInitError : public std::runtime_error + { + public: + LockInitError(const char* what) : std::runtime_error(what) + {}; + }; +}; + + +#endif /* GREENLET_THREAD_SUPPORT_HPP */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/__init__.py b/venv/lib/python3.11/site-packages/greenlet/platform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/platform/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a03d110 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/platform/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd b/venv/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd new file mode 100644 index 0000000..0928595 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd @@ -0,0 +1,2 @@ +call "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" amd64 +ml64 /nologo /c /Fo switch_x64_masm.obj switch_x64_masm.asm diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h new file mode 100644 index 0000000..058617c --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h @@ -0,0 +1,124 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-16 Add clang support using x register naming. Fredrik Fornwall + * 13-Apr-13 Add support for strange GCC caller-save decisions + * 08-Apr-13 File creation. Michael Matz + * + * NOTES + * + * Simply save all callee saved registers + * + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REGS_TO_SAVE "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", \ + "x27", "x28", "x30" /* aka lr */, \ + "v8", "v9", "v10", "v11", \ + "v12", "v13", "v14", "v15" + +/* + * Recall: + asm asm-qualifiers ( AssemblerTemplate + : OutputOperands + [ : InputOperands + [ : Clobbers ] ]) + + or (if asm-qualifiers contains 'goto') + + asm asm-qualifiers ( AssemblerTemplate + : OutputOperands + : InputOperands + : Clobbers + : GotoLabels) + + and OutputOperands are + + [ [asmSymbolicName] ] constraint (cvariablename) + + When a name is given, refer to it as ``%[the name]``. + When not given, ``%i`` where ``i`` is the zero-based index. + + constraints starting with ``=`` means only writing; ``+`` means + reading and writing. + + This is followed by ``r`` (must be register) or ``m`` (must be memory) + and these can be combined. + + The ``cvariablename`` is actually an lvalue expression. + + In AArch65, 31 general purpose registers. If named X0... they are + 64-bit. If named W0... they are the bottom 32 bits of the + corresponding 64 bit register. + + XZR and WZR are hardcoded to 0, and ignore writes. + + Arguments are in X0..X7. C++ uses X0 for ``this``. X0 holds simple return + values (?) + + Whenever a W register is written, the top half of the X register is zeroed. + */ + +static int +slp_switch(void) +{ + int err; + void *fp; + /* Windowz uses a 32-bit long on a 64-bit platform, unlike the rest of + the world, and in theory we can be compiled with GCC/llvm on 64-bit + windows. So we need a fixed-width type. + */ + int64_t *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("str x29, %0" : "=m"(fp) : : ); + __asm__ ("mov %0, sp" : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add sp,sp,%0\n" + "add x29,x29,%0\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + /* SLP_SAVE_STATE macro contains some return statements + (of -1 and 1). It falls through only when + the return value of slp_save_state() is zero, which + is placed in x0. + In that case we (slp_switch) also want to return zero + (also in x0 of course). + Now, some GCC versions (seen with 4.8) think it's a + good idea to save/restore x0 around the call to + slp_restore_state(), instead of simply zeroing it + at the return below. But slp_restore_state + writes random values to the stack slot used for this + save/restore (from when it once was saved above in + SLP_SAVE_STATE, when it was still uninitialized), so + "restoring" that precious zero actually makes us + return random values. There are some ways to make + GCC not use that zero value in the normal return path + (e.g. making err volatile, but that costs a little + stack space), and the simplest is to call a function + that returns an unknown value (which happens to be zero), + so the saved/restored value is unused. + + Thus, this line stores a 0 into the ``err`` variable + (which must be held in a register for this instruction, + of course). The ``w`` qualifier causes the instruction + to use W0 instead of X0, otherwise we get a warning + about a value size mismatch (because err is an int, + and aarch64 platforms are LP64: 32-bit int, 64 bit long + and pointer). + */ + __asm__ volatile ("mov %w0, #0" : "=r" (err)); + } + __asm__ volatile ("ldr x29, %0" : : "m" (fp) :); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return err; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h new file mode 100644 index 0000000..7e07abf --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h @@ -0,0 +1,30 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "$9", "$10", "$11", "$12", "$13", "$14", "$15", \ + "$f2", "$f3", "$f4", "$f5", "$f6", "$f7", "$f8", "$f9" + +static int +slp_switch(void) +{ + int ret; + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mov $30, %0" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addq $30, %0, $30\n\t" + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mov $31, %0" : "=r" (ret) : ); + return ret; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h new file mode 100644 index 0000000..d470110 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h @@ -0,0 +1,87 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 3-May-13 Ralf Schmitt + * Add support for strange GCC caller-save decisions + * (ported from switch_aarch64_gcc.h) + * 18-Aug-11 Alexey Borzenkov + * Correctly save rbp, csr and cw + * 01-Apr-04 Hye-Shik Chang + * Ported from i386 to amd64. + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for spark + * 31-Avr-02 Armin Rigo + * Added ebx, esi and edi register-saves. + * 01-Mar-02 Samual M. Rushing + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +/* #define STACK_MAGIC 3 */ +/* the above works fine with gcc 2.96, but 2.95.3 wants this */ +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "r12", "r13", "r14", "r15" + +static int +slp_switch(void) +{ + int err; + void* rbp; + void* rbx; + unsigned int csr; + unsigned short cw; + /* This used to be declared 'register', but that does nothing in + modern compilers and is explicitly forbidden in some new + standards. */ + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("fstcw %0" : "=m" (cw)); + __asm__ volatile ("stmxcsr %0" : "=m" (csr)); + __asm__ volatile ("movq %%rbp, %0" : "=m" (rbp)); + __asm__ volatile ("movq %%rbx, %0" : "=m" (rbx)); + __asm__ ("movq %%rsp, %0" : "=g" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addq %0, %%rsp\n" + "addq %0, %%rbp\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + __asm__ volatile ("xorq %%rax, %%rax" : "=a" (err)); + } + __asm__ volatile ("movq %0, %%rbx" : : "m" (rbx)); + __asm__ volatile ("movq %0, %%rbp" : : "m" (rbp)); + __asm__ volatile ("ldmxcsr %0" : : "m" (csr)); + __asm__ volatile ("fldcw %0" : : "m" (cw)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h new file mode 100644 index 0000000..655003a --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h @@ -0,0 +1,79 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 14-Aug-06 File creation. Ported from Arm Thumb. Sylvain Baro + * 3-Sep-06 Commented out saving of r1-r3 (r4 already commented out) as I + * read that these do not need to be saved. Also added notes and + * errors related to the frame pointer. Richard Tew. + * + * NOTES + * + * It is not possible to detect if fp is used or not, so the supplied + * switch function needs to support it, so that you can remove it if + * it does not apply to you. + * + * POSSIBLE ERRORS + * + * "fp cannot be used in asm here" + * + * - Try commenting out "fp" in REGS_TO_SAVE. + * + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REG_SP "sp" +#define REG_SPSP "sp,sp" +#ifdef __thumb__ +#define REG_FP "r7" +#define REG_FPFP "r7,r7" +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r8", "r9", "r10", "r11", "lr" +#else +#define REG_FP "fp" +#define REG_FPFP "fp,fp" +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r7", "r8", "r9", "r10", "lr" +#endif +#if defined(__SOFTFP__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL +#elif defined(__VFP_FP__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "d8", "d9", "d10", "d11", \ + "d12", "d13", "d14", "d15" +#elif defined(__MAVERICK__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "mvf4", "mvf5", "mvf6", "mvf7", \ + "mvf8", "mvf9", "mvf10", "mvf11", \ + "mvf12", "mvf13", "mvf14", "mvf15" +#else +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "f4", "f5", "f6", "f7" +#endif + +static int +#ifdef __GNUC__ +__attribute__((optimize("no-omit-frame-pointer"))) +#endif +slp_switch(void) +{ + void *fp; + int *stackref, stsizediff; + int result; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mov r0," REG_FP "\n\tstr r0,%0" : "=m" (fp) : : "r0"); + __asm__ ("mov %0," REG_SP : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add " REG_SPSP ",%0\n" + "add " REG_FPFP ",%0\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("ldr r0,%1\n\tmov " REG_FP ",r0\n\tmov %0, #0" : "=r" (result) : "m" (fp) : "r0"); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return result; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h new file mode 100644 index 0000000..9e640e1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h @@ -0,0 +1,67 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 31-May-15 iOS support. Ported from arm32. Proton + * + * NOTES + * + * It is not possible to detect if fp is used or not, so the supplied + * switch function needs to support it, so that you can remove it if + * it does not apply to you. + * + * POSSIBLE ERRORS + * + * "fp cannot be used in asm here" + * + * - Try commenting out "fp" in REGS_TO_SAVE. + * + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 +#define REG_SP "sp" +#define REG_SPSP "sp,sp" +#define REG_FP "r7" +#define REG_FPFP "r7,r7" +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r8", "r10", "r11", "lr" +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "d8", "d9", "d10", "d11", \ + "d12", "d13", "d14", "d15" + +static int +#ifdef __GNUC__ +__attribute__((optimize("no-omit-frame-pointer"))) +#endif +slp_switch(void) +{ + void *fp; + int *stackref, stsizediff, result; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("str " REG_FP ",%0" : "=m" (fp)); + __asm__ ("mov %0," REG_SP : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add " REG_SPSP ",%0\n" + "add " REG_FPFP ",%0\n" + : + : "r" (stsizediff) + : REGS_TO_SAVE /* Clobber registers, force compiler to + * recalculate address of void *fp from REG_SP or REG_FP */ + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ( + "ldr " REG_FP ", %1\n\t" + "mov %0, #0" + : "=r" (result) + : "m" (fp) + : REGS_TO_SAVE /* Force compiler to restore saved registers after this */ + ); + return result; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm new file mode 100644 index 0000000..29f9c22 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm @@ -0,0 +1,53 @@ + AREA switch_arm64_masm, CODE, READONLY; + GLOBAL slp_switch [FUNC] + EXTERN slp_save_state_asm + EXTERN slp_restore_state_asm + +slp_switch + ; push callee saved registers to stack + stp x19, x20, [sp, #-16]! + stp x21, x22, [sp, #-16]! + stp x23, x24, [sp, #-16]! + stp x25, x26, [sp, #-16]! + stp x27, x28, [sp, #-16]! + stp x29, x30, [sp, #-16]! + stp d8, d9, [sp, #-16]! + stp d10, d11, [sp, #-16]! + stp d12, d13, [sp, #-16]! + stp d14, d15, [sp, #-16]! + + ; call slp_save_state_asm with stack pointer + mov x0, sp + bl slp_save_state_asm + + ; early return for return value of 1 and -1 + cmp x0, #-1 + b.eq RETURN + cmp x0, #1 + b.eq RETURN + + ; increment stack and frame pointer + add sp, sp, x0 + add x29, x29, x0 + + bl slp_restore_state_asm + + ; store return value for successful completion of routine + mov x0, #0 + +RETURN + ; pop registers from stack + ldp d14, d15, [sp], #16 + ldp d12, d13, [sp], #16 + ldp d10, d11, [sp], #16 + ldp d8, d9, [sp], #16 + ldp x29, x30, [sp], #16 + ldp x27, x28, [sp], #16 + ldp x25, x26, [sp], #16 + ldp x23, x24, [sp], #16 + ldp x21, x22, [sp], #16 + ldp x19, x20, [sp], #16 + + ret + + END diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj new file mode 100644 index 0000000..f6f220e Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj differ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h new file mode 100644 index 0000000..7ab7f45 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h @@ -0,0 +1,17 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 21-Oct-21 Niyas Sait + * First version to enable win/arm64 support. + */ + +#define STACK_REFPLUS 1 +#define STACK_MAGIC 0 + +/* Use the generic support for an external assembly language slp_switch function. */ +#define EXTERNAL_ASM + +#ifdef SLP_EVAL +/* This always uses the external masm assembly file. */ +#endif \ No newline at end of file diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h new file mode 100644 index 0000000..ac469d3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h @@ -0,0 +1,48 @@ +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REG_FP "r8" +#ifdef __CSKYABIV2__ +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r7", "r9", "r10", "r11", "r15",\ + "r16", "r17", "r18", "r19", "r20", "r21", "r22",\ + "r23", "r24", "r25" + +#if defined (__CSKY_HARD_FLOAT__) || (__CSKY_VDSP__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "vr8", "vr9", "vr10", "vr11", "vr12",\ + "vr13", "vr14", "vr15" +#else +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL +#endif +#else +#define REGS_TO_SAVE "r9", "r10", "r11", "r12", "r13", "r15" +#endif + + +static int +#ifdef __GNUC__ +__attribute__((optimize("no-omit-frame-pointer"))) +#endif +slp_switch(void) +{ + int *stackref, stsizediff; + int result; + + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mov %0, sp" : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addu sp,%0\n" + "addu "REG_FP",%0\n" + : + : "r" (stsizediff) + ); + + SLP_RESTORE_STATE(); + } + __asm__ volatile ("movi %0, 0" : "=r" (result)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + + return result; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h new file mode 100644 index 0000000..9eaf34e --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h @@ -0,0 +1,31 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "s0", "s1", "s2", "s3", "s4", "s5", \ + "s6", "s7", "s8", "fp", \ + "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31" + +static int +slp_switch(void) +{ + int ret; + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("move %0, $sp" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add.d $sp, $sp, %0\n\t" + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("move %0, $zero" : "=r" (ret) : ); + return ret; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h new file mode 100644 index 0000000..da761c2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h @@ -0,0 +1,38 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 2014-01-06 Andreas Schwab + * File created. + */ + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "%d2", "%d3", "%d4", "%d5", "%d6", "%d7", \ + "%a2", "%a3", "%a4" + +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + void *fp, *a5; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("move.l %%fp, %0" : "=m"(fp)); + __asm__ volatile ("move.l %%a5, %0" : "=m"(a5)); + __asm__ ("move.l %%sp, %0" : "=r"(stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ("add.l %0, %%sp; add.l %0, %%fp" : : "r"(stsizediff)); + SLP_RESTORE_STATE(); + __asm__ volatile ("clr.l %0" : "=g" (err)); + } + __asm__ volatile ("move.l %0, %%a5" : : "m"(a5)); + __asm__ volatile ("move.l %0, %%fp" : : "m"(fp)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return err; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h new file mode 100644 index 0000000..50e70c4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h @@ -0,0 +1,65 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 20-Sep-14 Matt Madison + * Re-code the saving of the gp register for MIPS64. + * 05-Jan-08 Thiemo Seufer + * Ported from ppc. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "$16", "$17", "$18", "$19", "$20", "$21", "$22", \ + "$23", "$30" +__attribute__((nomips16)) +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; +#ifdef __mips64 + uint64_t gpsave; +#endif + __asm__ __volatile__ ("" : : : REGS_TO_SAVE); +#ifdef __mips64 + __asm__ __volatile__ ("sd $28,%0" : "=m" (gpsave) : : ); +#endif + __asm__ ("move %0, $29" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ __volatile__ ( +#ifdef __mips64 + "daddu $29, $29, %0\n" +#else + "addu $29, $29, %0\n" +#endif + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } +#ifdef __mips64 + __asm__ __volatile__ ("ld $28,%0" : : "m" (gpsave) : ); +#endif + __asm__ __volatile__ ("" : : : REGS_TO_SAVE); + __asm__ __volatile__ ("move %0, $0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h new file mode 100644 index 0000000..e7e0b87 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h @@ -0,0 +1,103 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 16-Oct-20 Jesse Gorzinski + * Copied from Linux PPC64 implementation + * 04-Sep-18 Alexey Borzenkov + * Workaround a gcc bug using manual save/restore of r30 + * 21-Mar-18 Tulio Magno Quites Machado Filho + * Added r30 to the list of saved registers in order to fully comply with + * both ppc64 ELFv1 ABI and the ppc64le ELFv2 ABI, that classify this + * register as a nonvolatile register used for local variables. + * 21-Mar-18 Laszlo Boszormenyi + * Save r2 (TOC pointer) manually. + * 10-Dec-13 Ulrich Weigand + * Support ELFv2 ABI. Save float/vector registers. + * 09-Mar-12 Michael Ellerman + * 64-bit implementation, copied from 32-bit. + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + * 31-Jul-12 Trevor Bowen + * Changed memory constraints to register only. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 6 + +#if defined(__ALTIVEC__) +#define ALTIVEC_REGS \ + "v20", "v21", "v22", "v23", "v24", "v25", "v26", "v27", \ + "v28", "v29", "v30", "v31", +#else +#define ALTIVEC_REGS +#endif + +#define REGS_TO_SAVE "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "r31", \ + "fr14", "fr15", "fr16", "fr17", "fr18", "fr19", "fr20", "fr21", \ + "fr22", "fr23", "fr24", "fr25", "fr26", "fr27", "fr28", "fr29", \ + "fr30", "fr31", \ + ALTIVEC_REGS \ + "cr2", "cr3", "cr4" + +static int +slp_switch(void) +{ + int err; + long *stackref, stsizediff; + void * toc; + void * r30; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("std 2, %0" : "=m" (toc)); + __asm__ volatile ("std 30, %0" : "=m" (r30)); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("ld 30, %0" : : "m" (r30)); + __asm__ volatile ("ld 2, %0" : : "m" (toc)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h new file mode 100644 index 0000000..3c324d0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h @@ -0,0 +1,105 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 04-Sep-18 Alexey Borzenkov + * Workaround a gcc bug using manual save/restore of r30 + * 21-Mar-18 Tulio Magno Quites Machado Filho + * Added r30 to the list of saved registers in order to fully comply with + * both ppc64 ELFv1 ABI and the ppc64le ELFv2 ABI, that classify this + * register as a nonvolatile register used for local variables. + * 21-Mar-18 Laszlo Boszormenyi + * Save r2 (TOC pointer) manually. + * 10-Dec-13 Ulrich Weigand + * Support ELFv2 ABI. Save float/vector registers. + * 09-Mar-12 Michael Ellerman + * 64-bit implementation, copied from 32-bit. + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + * 31-Jul-12 Trevor Bowen + * Changed memory constraints to register only. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#if _CALL_ELF == 2 +#define STACK_MAGIC 4 +#else +#define STACK_MAGIC 6 +#endif + +#if defined(__ALTIVEC__) +#define ALTIVEC_REGS \ + "v20", "v21", "v22", "v23", "v24", "v25", "v26", "v27", \ + "v28", "v29", "v30", "v31", +#else +#define ALTIVEC_REGS +#endif + +#define REGS_TO_SAVE "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "r31", \ + "fr14", "fr15", "fr16", "fr17", "fr18", "fr19", "fr20", "fr21", \ + "fr22", "fr23", "fr24", "fr25", "fr26", "fr27", "fr28", "fr29", \ + "fr30", "fr31", \ + ALTIVEC_REGS \ + "cr2", "cr3", "cr4" + +static int +slp_switch(void) +{ + int err; + long *stackref, stsizediff; + void * toc; + void * r30; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("std 2, %0" : "=m" (toc)); + __asm__ volatile ("std 30, %0" : "=m" (r30)); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("ld 30, %0" : : "m" (r30)); + __asm__ volatile ("ld 2, %0" : : "m" (toc)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h new file mode 100644 index 0000000..6d93c13 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h @@ -0,0 +1,87 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Mar-11 Floris Bruynooghe + * Do not add stsizediff to general purpose + * register (GPR) 30 as this is a non-volatile and + * unused by the PowerOpen Environment, therefore + * this was modifying a user register instead of the + * frame pointer (which does not seem to exist). + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h new file mode 100644 index 0000000..e83ad70 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h @@ -0,0 +1,84 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + * 31-Jul-12 Trevor Bowen + * Changed memory constraints to register only. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + "add 30, 30, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h new file mode 100644 index 0000000..bd414c6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h @@ -0,0 +1,82 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" + +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("; asm block 2\n\tmr %0, r1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "; asm block 3\n" + "\tmr r11, %0\n" + "\tadd r1, r1, r11\n" + "\tadd r30, r30, r11\n" + : /* no outputs */ + : "r" (stsizediff) + : "r11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h new file mode 100644 index 0000000..bb18808 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h @@ -0,0 +1,82 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + "add 30, 30, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h new file mode 100644 index 0000000..8761122 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h @@ -0,0 +1,41 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "s1", "s2", "s3", "s4", "s5", \ + "s6", "s7", "s8", "s9", "s10", "s11", "fs0", "fs1", \ + "fs2", "fs3", "fs4", "fs5", "fs6", "fs7", "fs8", "fs9", \ + "fs10", "fs11" + +static int +slp_switch(void) +{ + int ret; + long fp; + long *stackref, stsizediff; + + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mv %0, fp" : "=r" (fp) : ); + __asm__ volatile ("mv %0, sp" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add sp, sp, %0\n\t" + "add fp, fp, %0\n\t" + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); +#if __riscv_xlen == 32 + __asm__ volatile ("lw fp, %0" : : "m" (fp)); +#else + __asm__ volatile ("ld fp, %0" : : "m" (fp)); +#endif + __asm__ volatile ("mv %0, zero" : "=r" (ret) : ); + return ret; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h new file mode 100644 index 0000000..9199367 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h @@ -0,0 +1,87 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 25-Jan-12 Alexey Borzenkov + * Fixed Linux/S390 port to work correctly with + * different optimization options both on 31-bit + * and 64-bit. Thanks to Stefan Raabe for lots + * of testing. + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 06-Oct-02 Gustavo Niemeyer + * Ported to Linux/S390. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#ifdef __s390x__ +#define STACK_MAGIC 20 /* 20 * 8 = 160 bytes of function call area */ +#else +#define STACK_MAGIC 24 /* 24 * 4 = 96 bytes of function call area */ +#endif + +/* Technically, r11-r13 also need saving, but function prolog starts + with stm(g) and since there are so many saved registers already + it won't be optimized, resulting in all r6-r15 being saved */ +#define REGS_TO_SAVE "r6", "r7", "r8", "r9", "r10", "r14", \ + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", \ + "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15" + +static int +slp_switch(void) +{ + int ret; + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); +#ifdef __s390x__ + __asm__ volatile ("lgr %0, 15" : "=r" (stackref) : ); +#else + __asm__ volatile ("lr %0, 15" : "=r" (stackref) : ); +#endif + { + SLP_SAVE_STATE(stackref, stsizediff); +/* N.B. + r11 may be used as the frame pointer, and in that case it cannot be + clobbered and needs offsetting just like the stack pointer (but in cases + where frame pointer isn't used we might clobber it accidentally). What's + scary is that r11 is 2nd (and even 1st when GOT is used) callee saved + register that gcc would chose for surviving function calls. However, + since r6-r10 are clobbered above, their cost for reuse is reduced, so + gcc IRA will chose them over r11 (not seeing r11 is implicitly saved), + making it relatively safe to offset in all cases. :) */ + __asm__ volatile ( +#ifdef __s390x__ + "agr 15, %0\n\t" + "agr 11, %0" +#else + "ar 15, %0\n\t" + "ar 11, %0" +#endif + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("lhi %0, 0" : "=r" (ret) : ); + return ret; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h new file mode 100644 index 0000000..5ecc3b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h @@ -0,0 +1,36 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REGS_TO_SAVE "r8", "r9", "r10", "r11", "r13", \ + "fr12", "fr13", "fr14", "fr15" + +// r12 Global context pointer, GP +// r14 Frame pointer, FP +// r15 Stack pointer, SP + +static int +slp_switch(void) +{ + int err; + void* fp; + int *stackref, stsizediff; + __asm__ volatile("" : : : REGS_TO_SAVE); + __asm__ volatile("mov.l r14, %0" : "=m"(fp) : :); + __asm__("mov r15, %0" : "=r"(stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile( + "add %0, r15\n" + "add %0, r14\n" + : /* no outputs */ + : "r"(stsizediff)); + SLP_RESTORE_STATE(); + __asm__ volatile("mov r0, %0" : "=r"(err) : :); + } + __asm__ volatile("mov.l %0, r14" : : "m"(fp) :); + __asm__ volatile("" : : : REGS_TO_SAVE); + return err; +} + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h new file mode 100644 index 0000000..96990c3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h @@ -0,0 +1,92 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 16-May-15 Alexey Borzenkov + * Move stack spilling code inside save/restore functions + * 30-Aug-13 Floris Bruynooghe + Clean the register windows again before returning. + This does not clobber the PIC register as it leaves + the current window intact and is required for multi- + threaded code to work correctly. + * 08-Mar-11 Floris Bruynooghe + * No need to set return value register explicitly + * before the stack and framepointer are adjusted + * as none of the other registers are influenced by + * this. Also don't needlessly clean the windows + * ('ta %0" :: "i" (ST_CLEAN_WINDOWS)') as that + * clobbers the gcc PIC register (%l7). + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * added support for SunOS sparc with gcc + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + + +#define STACK_MAGIC 0 + + +#if defined(__sparcv9) +#define SLP_FLUSHW __asm__ volatile ("flushw") +#else +#define SLP_FLUSHW __asm__ volatile ("ta 3") /* ST_FLUSH_WINDOWS */ +#endif + +/* On sparc we need to spill register windows inside save/restore functions */ +#define SLP_BEFORE_SAVE_STATE() SLP_FLUSHW +#define SLP_BEFORE_RESTORE_STATE() SLP_FLUSHW + + +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + + /* Put current stack pointer into stackref. + * Register spilling is done in save/restore. + */ + __asm__ volatile ("mov %%sp, %0" : "=r" (stackref)); + + { + /* Thou shalt put SLP_SAVE_STATE into a local block */ + /* Copy the current stack onto the heap */ + SLP_SAVE_STATE(stackref, stsizediff); + + /* Increment stack and frame pointer by stsizediff */ + __asm__ volatile ( + "add %0, %%sp, %%sp\n\t" + "add %0, %%fp, %%fp" + : : "r" (stsizediff)); + + /* Copy new stack from it's save store on the heap */ + SLP_RESTORE_STATE(); + + __asm__ volatile ("mov %1, %0" : "=r" (err) : "i" (0)); + return err; + } +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h new file mode 100644 index 0000000..893369c --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h @@ -0,0 +1,63 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 17-Aug-12 Fantix King + * Ported from amd64. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "r12", "r13", "r14", "r15" + + +static int +slp_switch(void) +{ + void* ebp; + void* ebx; + unsigned int csr; + unsigned short cw; + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("fstcw %0" : "=m" (cw)); + __asm__ volatile ("stmxcsr %0" : "=m" (csr)); + __asm__ volatile ("movl %%ebp, %0" : "=m" (ebp)); + __asm__ volatile ("movl %%ebx, %0" : "=m" (ebx)); + __asm__ ("movl %%esp, %0" : "=g" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addl %0, %%esp\n" + "addl %0, %%ebp\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("movl %0, %%ebx" : : "m" (ebx)); + __asm__ volatile ("movl %0, %%ebp" : : "m" (ebp)); + __asm__ volatile ("ldmxcsr %0" : : "m" (csr)); + __asm__ volatile ("fldcw %0" : : "m" (cw)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("xorl %%eax, %%eax" : "=a" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm new file mode 100644 index 0000000..f5c72a2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm @@ -0,0 +1,111 @@ +; +; stack switching code for MASM on x641 +; Kristjan Valur Jonsson, sept 2005 +; + + +;prototypes for our calls +slp_save_state_asm PROTO +slp_restore_state_asm PROTO + + +pushxmm MACRO reg + sub rsp, 16 + .allocstack 16 + movaps [rsp], reg ; faster than movups, but we must be aligned + ; .savexmm128 reg, offset (don't know what offset is, no documentation) +ENDM +popxmm MACRO reg + movaps reg, [rsp] ; faster than movups, but we must be aligned + add rsp, 16 +ENDM + +pushreg MACRO reg + push reg + .pushreg reg +ENDM +popreg MACRO reg + pop reg +ENDM + + +.code +slp_switch PROC FRAME + ;realign stack to 16 bytes after return address push, makes the following faster + sub rsp,8 + .allocstack 8 + + pushxmm xmm15 + pushxmm xmm14 + pushxmm xmm13 + pushxmm xmm12 + pushxmm xmm11 + pushxmm xmm10 + pushxmm xmm9 + pushxmm xmm8 + pushxmm xmm7 + pushxmm xmm6 + + pushreg r15 + pushreg r14 + pushreg r13 + pushreg r12 + + pushreg rbp + pushreg rbx + pushreg rdi + pushreg rsi + + sub rsp, 10h ;allocate the singlefunction argument (must be multiple of 16) + .allocstack 10h +.endprolog + + lea rcx, [rsp+10h] ;load stack base that we are saving + call slp_save_state_asm ;pass stackpointer, return offset in eax + cmp rax, 1 + je EXIT1 + cmp rax, -1 + je EXIT2 + ;actual stack switch: + add rsp, rax + call slp_restore_state_asm + xor rax, rax ;return 0 + +EXIT: + + add rsp, 10h + popreg rsi + popreg rdi + popreg rbx + popreg rbp + + popreg r12 + popreg r13 + popreg r14 + popreg r15 + + popxmm xmm6 + popxmm xmm7 + popxmm xmm8 + popxmm xmm9 + popxmm xmm10 + popxmm xmm11 + popxmm xmm12 + popxmm xmm13 + popxmm xmm14 + popxmm xmm15 + + add rsp, 8 + ret + +EXIT1: + mov rax, 1 + jmp EXIT + +EXIT2: + sar rax, 1 + jmp EXIT + +slp_switch ENDP + +END \ No newline at end of file diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj new file mode 100644 index 0000000..64e3e6b Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj differ diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h new file mode 100644 index 0000000..601ea56 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h @@ -0,0 +1,60 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 26-Sep-02 Christian Tismer + * again as a result of virtualized stack access, + * the compiler used less registers. Needed to + * explicit mention registers in order to get them saved. + * Thanks to Jeff Senn for pointing this out and help. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 01-Mar-02 Christian Tismer + * Initial final version after lots of iterations for i386. + */ + +/* Avoid alloca redefined warning on mingw64 */ +#ifndef alloca +#define alloca _alloca +#endif + +#define STACK_REFPLUS 1 +#define STACK_MAGIC 0 + +/* Use the generic support for an external assembly language slp_switch function. */ +#define EXTERNAL_ASM + +#ifdef SLP_EVAL +/* This always uses the external masm assembly file. */ +#endif + +/* + * further self-processing support + */ + +/* we have IsBadReadPtr available, so we can peek at objects */ +/* +#define STACKLESS_SPY + +#ifdef IMPLEMENT_STACKLESSMODULE +#include "Windows.h" +#define CANNOT_READ_MEM(p, bytes) IsBadReadPtr(p, bytes) + +static int IS_ON_STACK(void*p) +{ + int stackref; + intptr_t stackbase = ((intptr_t)&stackref) & 0xfffff000; + return (intptr_t)p >= stackbase && (intptr_t)p < stackbase + 0x00100000; +} + +#endif +*/ \ No newline at end of file diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h new file mode 100644 index 0000000..0f3a59f --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h @@ -0,0 +1,326 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 26-Sep-02 Christian Tismer + * again as a result of virtualized stack access, + * the compiler used less registers. Needed to + * explicit mention registers in order to get them saved. + * Thanks to Jeff Senn for pointing this out and help. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 01-Mar-02 Christian Tismer + * Initial final version after lots of iterations for i386. + */ + +#define alloca _alloca + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +/* Some magic to quell warnings and keep slp_switch() from crashing when built + with VC90. Disable global optimizations, and the warning: frame pointer + register 'ebp' modified by inline assembly code. + + We used to just disable global optimizations ("g") but upstream stackless + Python, as well as stackman, turn off all optimizations. + +References: +https://github.com/stackless-dev/stackman/blob/dbc72fe5207a2055e658c819fdeab9731dee78b9/stackman/platforms/switch_x86_msvc.h +https://github.com/stackless-dev/stackless/blob/main-slp/Stackless/platf/switch_x86_msvc.h +*/ +#define WIN32_LEAN_AND_MEAN +#include + +#pragma optimize("", off) /* so that autos are stored on the stack */ +#pragma warning(disable:4731) +#pragma warning(disable:4733) /* disable warning about modifying FS[0] */ + +/** + * Most modern compilers and environments handle C++ exceptions without any + * special help from us. MSVC on 32-bit windows is an exception. There, C++ + * exceptions are dealt with using Windows' Structured Exception Handling + * (SEH). + * + * SEH is implemented as a singly linked list of nodes. The + * head of this list is stored in the Thread Information Block, which itself + * is pointed to from the FS register. It's the first field in the structure, + * or offset 0, so we can access it using assembly FS:[0], or the compiler + * intrinsics and field offset information from the headers (as we do below). + * Somewhat unusually, the tail of the list doesn't have prev == NULL, it has + * prev == 0xFFFFFFFF. + * + * SEH was designed for C, and traditionally uses the MSVC compiler + * intrinsincs __try{}/__except{}. It is also utilized for C++ exceptions by + * MSVC; there, every throw of a C++ exception raises a SEH error with the + * ExceptionCode 0xE06D7363; the SEH handler list is then traversed to + * deal with the exception. + * + * If the SEH list is corrupt, then when a C++ exception is thrown the program + * will abruptly exit with exit code 1. This does not use std::terminate(), so + * std::set_terminate() is useless to debug this. + * + * The SEH list is closely tied to the call stack; entering a function that + * uses __try{} or most C++ functions will push a new handler onto the front + * of the list. Returning from the function will remove the handler. Saving + * and restoring the head node of the SEH list (FS:[0]) per-greenlet is NOT + * ENOUGH to make SEH or exceptions work. + * + * Stack switching breaks SEH because the call stack no longer necessarily + * matches the SEH list. For example, given greenlet A that switches to + * greenlet B, at the moment of entering greenlet B, we will have any SEH + * handlers from greenlet A on the SEH list; greenlet B can then add its own + * handlers to the SEH list. When greenlet B switches back to greenlet A, + * greenlet B's handlers would still be on the SEH stack, but when switch() + * returns control to greenlet A, we have replaced the contents of the stack + * in memory, so all the address that greenlet B added to the SEH list are now + * invalid: part of the call stack has been unwound, but the SEH list was out + * of sync with the call stack. The net effect is that exception handling + * stops working. + * + * Thus, when switching greenlets, we need to be sure that the SEH list + * matches the effective call stack, "cutting out" any handlers that were + * pushed by the greenlet that switched out and which are no longer valid. + * + * The easiest way to do this is to capture the SEH list at the time the main + * greenlet for a thread is created, and, when initially starting a greenlet, + * start a new SEH list for it, which contains nothing but the handler + * established for the new greenlet itself, with the tail being the handlers + * for the main greenlet. If we then save and restore the SEH per-greenlet, + * they won't interfere with each others SEH lists. (No greenlet can unwind + * the call stack past the handlers established by the main greenlet). + * + * By observation, a new thread starts with three SEH handlers on the list. By + * the time we get around to creating the main greenlet, though, there can be + * many more, established by transient calls that lead to the creation of the + * main greenlet. Therefore, 3 is a magic constant telling us when to perform + * the initial slice. + * + * All of this can be debugged using a vectored exception handler, which + * operates independently of the SEH handler list, and is called first. + * Walking the SEH list at key points can also be helpful. + * + * References: + * https://en.wikipedia.org/wiki/Win32_Thread_Information_Block + * https://devblogs.microsoft.com/oldnewthing/20100730-00/?p=13273 + * https://docs.microsoft.com/en-us/cpp/cpp/try-except-statement?view=msvc-160 + * https://docs.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-160 + * https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling + * https://docs.microsoft.com/en-us/windows/win32/debug/using-a-vectored-exception-handler + * https://bytepointer.com/resources/pietrek_crash_course_depths_of_win32_seh.htm + */ +#define GREENLET_NEEDS_EXCEPTION_STATE_SAVED + + +typedef struct _GExceptionRegistration { + struct _GExceptionRegistration* prev; + void* handler_f; +} GExceptionRegistration; + +static void +slp_set_exception_state(const void *const seh_state) +{ + // Because the stack from from which we do this is ALSO a handler, and + // that one we want to keep, we need to relink the current SEH handler + // frame to point to this one, cutting out the middle men, as it were. + // + // Entering a try block doesn't change the SEH frame, but entering a + // function containing a try block does. + GExceptionRegistration* current_seh_state = (GExceptionRegistration*)__readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + current_seh_state->prev = (GExceptionRegistration*)seh_state; +} + + +static GExceptionRegistration* +x86_slp_get_third_oldest_handler() +{ + GExceptionRegistration* a = NULL; /* Closest to the top */ + GExceptionRegistration* b = NULL; /* second */ + GExceptionRegistration* c = NULL; + GExceptionRegistration* seh_state = (GExceptionRegistration*)__readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + a = b = c = seh_state; + + while (seh_state && seh_state != (GExceptionRegistration*)0xFFFFFFFF) { + if ((void*)seh_state->prev < (void*)100) { + fprintf(stderr, "\tERROR: Broken SEH chain.\n"); + return NULL; + } + a = b; + b = c; + c = seh_state; + + seh_state = seh_state->prev; + } + return a ? a : (b ? b : c); +} + + +static void* +slp_get_exception_state() +{ + // XXX: There appear to be three SEH handlers on the stack already at the + // start of the thread. Is that a guarantee? Almost certainly not. Yet in + // all observed cases it has been three. This is consistent with + // faulthandler off or on, and optimizations off or on. It may not be + // consistent with other operating system versions, though: we only have + // CI on one or two versions (don't ask what there are). + // In theory we could capture the number of handlers on the chain when + // PyInit__greenlet is called: there are probably only the default + // handlers at that point (unless we're embedded and people have used + // __try/__except or a C++ handler)? + return x86_slp_get_third_oldest_handler(); +} + +static int +slp_switch(void) +{ + /* MASM syntax is typically reversed from other assemblers. + It is usually + */ + int *stackref, stsizediff; + /* store the structured exception state for this stack */ + DWORD seh_state = __readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + __asm mov stackref, esp; + /* modify EBX, ESI and EDI in order to get them preserved */ + __asm mov ebx, ebx; + __asm xchg esi, edi; + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm { + mov eax, stsizediff + add esp, eax + add ebp, eax + } + SLP_RESTORE_STATE(); + } + __writefsdword(FIELD_OFFSET(NT_TIB, ExceptionList), seh_state); + return 0; +} + +/* re-enable ebp warning and global optimizations. */ +#pragma optimize("", on) +#pragma warning(default:4731) +#pragma warning(default:4733) /* disable warning about modifying FS[0] */ + + +#endif + +/* + * further self-processing support + */ + +/* we have IsBadReadPtr available, so we can peek at objects */ +#define STACKLESS_SPY + +#ifdef GREENLET_DEBUG + +#define CANNOT_READ_MEM(p, bytes) IsBadReadPtr(p, bytes) + +static int IS_ON_STACK(void*p) +{ + int stackref; + int stackbase = ((int)&stackref) & 0xfffff000; + return (int)p >= stackbase && (int)p < stackbase + 0x00100000; +} + +static void +x86_slp_show_seh_chain() +{ + GExceptionRegistration* seh_state = (GExceptionRegistration*)__readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + fprintf(stderr, "====== SEH Chain ======\n"); + while (seh_state && seh_state != (GExceptionRegistration*)0xFFFFFFFF) { + fprintf(stderr, "\tSEH_chain addr: %p handler: %p prev: %p\n", + seh_state, + seh_state->handler_f, seh_state->prev); + if ((void*)seh_state->prev < (void*)100) { + fprintf(stderr, "\tERROR: Broken chain.\n"); + break; + } + seh_state = seh_state->prev; + } + fprintf(stderr, "====== End SEH Chain ======\n"); + fflush(NULL); + return; +} + +//addVectoredExceptionHandler constants: +//CALL_FIRST means call this exception handler first; +//CALL_LAST means call this exception handler last +#define CALL_FIRST 1 +#define CALL_LAST 0 + +LONG WINAPI +GreenletVectorHandler(PEXCEPTION_POINTERS ExceptionInfo) +{ + // We get one of these for every C++ exception, with code + // E06D7363 + // This is a special value that means "C++ exception from MSVC" + // https://devblogs.microsoft.com/oldnewthing/20100730-00/?p=13273 + // + // Install in the module init function with: + // AddVectoredExceptionHandler(CALL_FIRST, GreenletVectorHandler); + PEXCEPTION_RECORD ExceptionRecord = ExceptionInfo->ExceptionRecord; + + fprintf(stderr, + "GOT VECTORED EXCEPTION:\n" + "\tExceptionCode : %p\n" + "\tExceptionFlags : %p\n" + "\tExceptionAddr : %p\n" + "\tNumberparams : %ld\n", + ExceptionRecord->ExceptionCode, + ExceptionRecord->ExceptionFlags, + ExceptionRecord->ExceptionAddress, + ExceptionRecord->NumberParameters + ); + if (ExceptionRecord->ExceptionFlags & 1) { + fprintf(stderr, "\t\tEH_NONCONTINUABLE\n" ); + } + if (ExceptionRecord->ExceptionFlags & 2) { + fprintf(stderr, "\t\tEH_UNWINDING\n" ); + } + if (ExceptionRecord->ExceptionFlags & 4) { + fprintf(stderr, "\t\tEH_EXIT_UNWIND\n" ); + } + if (ExceptionRecord->ExceptionFlags & 8) { + fprintf(stderr, "\t\tEH_STACK_INVALID\n" ); + } + if (ExceptionRecord->ExceptionFlags & 0x10) { + fprintf(stderr, "\t\tEH_NESTED_CALL\n" ); + } + if (ExceptionRecord->ExceptionFlags & 0x20) { + fprintf(stderr, "\t\tEH_TARGET_UNWIND\n" ); + } + if (ExceptionRecord->ExceptionFlags & 0x40) { + fprintf(stderr, "\t\tEH_COLLIDED_UNWIND\n" ); + } + fprintf(stderr, "\n"); + fflush(NULL); + for(DWORD i = 0; i < ExceptionRecord->NumberParameters; i++) { + fprintf(stderr, "\t\t\tParam %ld: %lX\n", i, ExceptionRecord->ExceptionInformation[i]); + } + + if (ExceptionRecord->NumberParameters == 3) { + fprintf(stderr, "\tAbout to traverse SEH chain\n"); + // C++ Exception records have 3 params. + x86_slp_show_seh_chain(); + } + + return EXCEPTION_CONTINUE_SEARCH; +} + + + + +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h new file mode 100644 index 0000000..493fa6b --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h @@ -0,0 +1,105 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 3-May-13 Ralf Schmitt + * Add support for strange GCC caller-save decisions + * (ported from switch_aarch64_gcc.h) + * 19-Aug-11 Alexey Borzenkov + * Correctly save ebp, ebx and cw + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'ebx' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for spark + * 31-Avr-02 Armin Rigo + * Added ebx, esi and edi register-saves. + * 01-Mar-02 Samual M. Rushing + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +/* #define STACK_MAGIC 3 */ +/* the above works fine with gcc 2.96, but 2.95.3 wants this */ +#define STACK_MAGIC 0 + +#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5) +# define ATTR_NOCLONE __attribute__((noclone)) +#else +# define ATTR_NOCLONE +#endif + +static int +slp_switch(void) +{ + int err; +#ifdef _WIN32 + void *seh; +#endif + void *ebp, *ebx; + unsigned short cw; + int *stackref, stsizediff; + __asm__ volatile ("" : : : "esi", "edi"); + __asm__ volatile ("fstcw %0" : "=m" (cw)); + __asm__ volatile ("movl %%ebp, %0" : "=m" (ebp)); + __asm__ volatile ("movl %%ebx, %0" : "=m" (ebx)); +#ifdef _WIN32 + __asm__ volatile ( + "movl %%fs:0x0, %%eax\n" + "movl %%eax, %0\n" + : "=m" (seh) + : + : "eax"); +#endif + __asm__ ("movl %%esp, %0" : "=g" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addl %0, %%esp\n" + "addl %0, %%ebp\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + __asm__ volatile ("xorl %%eax, %%eax" : "=a" (err)); + } +#ifdef _WIN32 + __asm__ volatile ( + "movl %0, %%eax\n" + "movl %%eax, %%fs:0x0\n" + : + : "m" (seh) + : "eax"); +#endif + __asm__ volatile ("movl %0, %%ebx" : : "m" (ebx)); + __asm__ volatile ("movl %0, %%ebp" : : "m" (ebp)); + __asm__ volatile ("fldcw %0" : : "m" (cw)); + __asm__ volatile ("" : : : "esi", "edi"); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/venv/lib/python3.11/site-packages/greenlet/slp_platformselect.h b/venv/lib/python3.11/site-packages/greenlet/slp_platformselect.h new file mode 100644 index 0000000..d9b7d0a --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/slp_platformselect.h @@ -0,0 +1,77 @@ +/* + * Platform Selection for Stackless Python + */ +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(MS_WIN32) && !defined(MS_WIN64) && defined(_M_IX86) && defined(_MSC_VER) +# include "platform/switch_x86_msvc.h" /* MS Visual Studio on X86 */ +#elif defined(MS_WIN64) && defined(_M_X64) && defined(_MSC_VER) || defined(__MINGW64__) +# include "platform/switch_x64_msvc.h" /* MS Visual Studio on X64 */ +#elif defined(MS_WIN64) && defined(_M_ARM64) +# include "platform/switch_arm64_msvc.h" /* MS Visual Studio on ARM64 */ +#elif defined(__GNUC__) && defined(__amd64__) && defined(__ILP32__) +# include "platform/switch_x32_unix.h" /* gcc on amd64 with x32 ABI */ +#elif defined(__GNUC__) && defined(__amd64__) +# include "platform/switch_amd64_unix.h" /* gcc on amd64 */ +#elif defined(__GNUC__) && defined(__i386__) +# include "platform/switch_x86_unix.h" /* gcc on X86 */ +#elif defined(__GNUC__) && defined(__powerpc64__) && (defined(__linux__) || defined(__FreeBSD__)) +# include "platform/switch_ppc64_linux.h" /* gcc on PowerPC 64-bit */ +#elif defined(__GNUC__) && defined(__PPC__) && (defined(__linux__) || defined(__FreeBSD__)) +# include "platform/switch_ppc_linux.h" /* gcc on PowerPC */ +#elif defined(__GNUC__) && defined(__POWERPC__) && defined(__APPLE__) +# include "platform/switch_ppc_macosx.h" /* Apple MacOS X on 32-bit PowerPC */ +#elif defined(__GNUC__) && defined(__powerpc64__) && defined(_AIX) +# include "platform/switch_ppc64_aix.h" /* gcc on AIX/PowerPC 64-bit */ +#elif defined(__GNUC__) && defined(_ARCH_PPC) && defined(_AIX) +# include "platform/switch_ppc_aix.h" /* gcc on AIX/PowerPC */ +#elif defined(__GNUC__) && defined(__powerpc__) && defined(__NetBSD__) +#include "platform/switch_ppc_unix.h" /* gcc on NetBSD/powerpc */ +#elif defined(__GNUC__) && defined(sparc) +# include "platform/switch_sparc_sun_gcc.h" /* SunOS sparc with gcc */ +#elif defined(__GNUC__) && defined(__sparc__) +# include "platform/switch_sparc_sun_gcc.h" /* NetBSD sparc with gcc */ +#elif defined(__SUNPRO_C) && defined(sparc) && defined(sun) +# include "platform/switch_sparc_sun_gcc.h" /* SunStudio on amd64 */ +#elif defined(__SUNPRO_C) && defined(__amd64__) && defined(sun) +# include "platform/switch_amd64_unix.h" /* SunStudio on amd64 */ +#elif defined(__SUNPRO_C) && defined(__i386__) && defined(sun) +# include "platform/switch_x86_unix.h" /* SunStudio on x86 */ +#elif defined(__GNUC__) && defined(__s390__) && defined(__linux__) +# include "platform/switch_s390_unix.h" /* Linux/S390 */ +#elif defined(__GNUC__) && defined(__s390x__) && defined(__linux__) +# include "platform/switch_s390_unix.h" /* Linux/S390 zSeries (64-bit) */ +#elif defined(__GNUC__) && defined(__arm__) +# ifdef __APPLE__ +# include +# endif +# if TARGET_OS_IPHONE +# include "platform/switch_arm32_ios.h" /* iPhone OS on arm32 */ +# else +# include "platform/switch_arm32_gcc.h" /* gcc using arm32 */ +# endif +#elif defined(__GNUC__) && defined(__mips__) && defined(__linux__) +# include "platform/switch_mips_unix.h" /* Linux/MIPS */ +#elif defined(__GNUC__) && defined(__aarch64__) +# include "platform/switch_aarch64_gcc.h" /* Aarch64 ABI */ +#elif defined(__GNUC__) && defined(__mc68000__) +# include "platform/switch_m68k_gcc.h" /* gcc on m68k */ +#elif defined(__GNUC__) && defined(__csky__) +#include "platform/switch_csky_gcc.h" /* gcc on csky */ +# elif defined(__GNUC__) && defined(__riscv) +# include "platform/switch_riscv_unix.h" /* gcc on RISC-V */ +#elif defined(__GNUC__) && defined(__alpha__) +# include "platform/switch_alpha_unix.h" /* gcc on DEC Alpha */ +#elif defined(MS_WIN32) && defined(__llvm__) && defined(__aarch64__) +# include "platform/switch_aarch64_gcc.h" /* LLVM Aarch64 ABI for Windows */ +#elif defined(__GNUC__) && defined(__loongarch64) && defined(__linux__) +# include "platform/switch_loongarch64_linux.h" /* LoongArch64 */ +#elif defined(__GNUC__) && defined(__sh__) +# include "platform/switch_sh_gcc.h" /* SuperH */ +#endif + +#ifdef __cplusplus +}; +#endif diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__init__.py b/venv/lib/python3.11/site-packages/greenlet/tests/__init__.py new file mode 100644 index 0000000..1861360 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/__init__.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +""" +Tests for greenlet. + +""" +import os +import sys +import sysconfig +import unittest + +from gc import collect +from gc import get_objects +from threading import active_count as active_thread_count +from time import sleep +from time import time + +import psutil + +from greenlet import greenlet as RawGreenlet +from greenlet import getcurrent + +from greenlet._greenlet import get_pending_cleanup_count +from greenlet._greenlet import get_total_main_greenlets + +from . import leakcheck + +PY312 = sys.version_info[:2] >= (3, 12) +PY313 = sys.version_info[:2] >= (3, 13) +# XXX: First tested on 3.14a7. Revisit all uses of this on later versions to ensure they +# are still valid. +PY314 = sys.version_info[:2] >= (3, 14) + +WIN = sys.platform.startswith("win") +RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS') +RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS +RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR') +RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR +RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX') + +# Is the current interpreter free-threaded?) Note that this +# isn't the same as whether the GIL is enabled, this is the build-time +# value. Certain CPython details, like the garbage collector, +# work very differently on potentially-free-threaded builds than +# standard builds. +RUNNING_ON_FREETHREAD_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + +class TestCaseMetaClass(type): + # wrap each test method with + # a) leak checks + def __new__(cls, classname, bases, classDict): + # pylint and pep8 fight over what this should be called (mcs or cls). + # pylint gets it right, but we can't scope disable pep8, so we go with + # its convention. + # pylint: disable=bad-mcs-classmethod-argument + check_totalrefcount = True + + # Python 3: must copy, we mutate the classDict. Interestingly enough, + # it doesn't actually error out, but under 3.6 we wind up wrapping + # and re-wrapping the same items over and over and over. + for key, value in list(classDict.items()): + if key.startswith('test') and callable(value): + classDict.pop(key) + if check_totalrefcount: + value = leakcheck.wrap_refcount(value) + classDict[key] = value + return type.__new__(cls, classname, bases, classDict) + + +class TestCase(unittest.TestCase, metaclass=TestCaseMetaClass): + + cleanup_attempt_sleep_duration = 0.001 + cleanup_max_sleep_seconds = 1 + + def wait_for_pending_cleanups(self, + initial_active_threads=None, + initial_main_greenlets=None): + initial_active_threads = initial_active_threads or self.threads_before_test + initial_main_greenlets = initial_main_greenlets or self.main_greenlets_before_test + sleep_time = self.cleanup_attempt_sleep_duration + # NOTE: This is racy! A Python-level thread object may be dead + # and gone, but the C thread may not yet have fired its + # destructors and added to the queue. There's no particular + # way to know that's about to happen. We try to watch the + # Python threads to make sure they, at least, have gone away. + # Counting the main greenlets, which we can easily do deterministically, + # also helps. + + # Always sleep at least once to let other threads run + sleep(sleep_time) + quit_after = time() + self.cleanup_max_sleep_seconds + # TODO: We could add an API that calls us back when a particular main greenlet is deleted? + # It would have to drop the GIL + while ( + get_pending_cleanup_count() + or active_thread_count() > initial_active_threads + or (not self.expect_greenlet_leak + and get_total_main_greenlets() > initial_main_greenlets)): + sleep(sleep_time) + if time() > quit_after: + print("Time limit exceeded.") + print("Threads: Waiting for only", initial_active_threads, + "-->", active_thread_count()) + print("MGlets : Waiting for only", initial_main_greenlets, + "-->", get_total_main_greenlets()) + break + collect() + + def count_objects(self, kind=list, exact_kind=True): + # pylint:disable=unidiomatic-typecheck + # Collect the garbage. + for _ in range(3): + collect() + if exact_kind: + return sum( + 1 + for x in get_objects() + if type(x) is kind + ) + # instances + return sum( + 1 + for x in get_objects() + if isinstance(x, kind) + ) + + greenlets_before_test = 0 + threads_before_test = 0 + main_greenlets_before_test = 0 + expect_greenlet_leak = False + + def count_greenlets(self): + """ + Find all the greenlets and subclasses tracked by the GC. + """ + return self.count_objects(RawGreenlet, False) + + def setUp(self): + # Ensure the main greenlet exists, otherwise the first test + # gets a false positive leak + super().setUp() + getcurrent() + self.threads_before_test = active_thread_count() + self.main_greenlets_before_test = get_total_main_greenlets() + self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test) + self.greenlets_before_test = self.count_greenlets() + + def tearDown(self): + if getattr(self, 'skipTearDown', False): + return + + self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test) + super().tearDown() + + def get_expected_returncodes_for_aborted_process(self): + import signal + # The child should be aborted in an unusual way. On POSIX + # platforms, this is done with abort() and signal.SIGABRT, + # which is reflected in a negative return value; however, on + # Windows, even though we observe the child print "Fatal + # Python error: Aborted" and in older versions of the C + # runtime "This application has requested the Runtime to + # terminate it in an unusual way," it always has an exit code + # of 3. This is interesting because 3 is the error code for + # ERROR_PATH_NOT_FOUND; BUT: the C runtime abort() function + # also uses this code. + # + # If we link to the static C library on Windows, the error + # code changes to '0xc0000409' (hex(3221226505)), which + # apparently is STATUS_STACK_BUFFER_OVERRUN; but "What this + # means is that nowadays when you get a + # STATUS_STACK_BUFFER_OVERRUN, it doesn’t actually mean that + # there is a stack buffer overrun. It just means that the + # application decided to terminate itself with great haste." + # + # + # On windows, we've also seen '0xc0000005' (hex(3221225477)). + # That's "Access Violation" + # + # See + # https://devblogs.microsoft.com/oldnewthing/20110519-00/?p=10623 + # and + # https://docs.microsoft.com/en-us/previous-versions/k089yyh0(v=vs.140)?redirectedfrom=MSDN + # and + # https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655 + expected_exit = ( + -signal.SIGABRT, + # But beginning on Python 3.11, the faulthandler + # that prints the C backtraces sometimes segfaults after + # reporting the exception but before printing the stack. + # This has only been seen on linux/gcc. + -signal.SIGSEGV, + ) if not WIN else ( + 3, + 0xc0000409, + 0xc0000005, + ) + return expected_exit + + def get_process_uss(self): + """ + Return the current process's USS in bytes. + + uss is available on Linux, macOS, Windows. Also known as + "Unique Set Size", this is the memory which is unique to a + process and which would be freed if the process was terminated + right now. + + If this is not supported by ``psutil``, this raises the + :exc:`unittest.SkipTest` exception. + """ + try: + return psutil.Process().memory_full_info().uss + except AttributeError as e: + raise unittest.SkipTest("uss not supported") from e + + def run_script(self, script_name, show_output=True): + import subprocess + script = os.path.join( + os.path.dirname(__file__), + script_name, + ) + + try: + return subprocess.check_output([sys.executable, script], + encoding='utf-8', + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as ex: + if show_output: + print('-----') + print('Failed to run script', script) + print('~~~~~') + print(ex.output) + print('------') + raise + + + def assertScriptRaises(self, script_name, exitcodes=None): + import subprocess + with self.assertRaises(subprocess.CalledProcessError) as exc: + output = self.run_script(script_name, show_output=False) + __traceback_info__ = output + # We're going to fail the assertion if we get here, at least + # preserve the output in the traceback. + + if exitcodes is None: + exitcodes = self.get_expected_returncodes_for_aborted_process() + self.assertIn(exc.exception.returncode, exitcodes) + return exc.exception diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..db0bf72 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_c_stack_refs_suspended_gc.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_c_stack_refs_suspended_gc.cpython-311.pyc new file mode 100644 index 0000000..bf3a34d Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_c_stack_refs_suspended_gc.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_clearing_run_switches.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_clearing_run_switches.cpython-311.pyc new file mode 100644 index 0000000..0b0b536 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_clearing_run_switches.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_cpp_exception.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_cpp_exception.cpython-311.pyc new file mode 100644 index 0000000..1db9e5a Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_cpp_exception.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_initialstub_already_started.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_initialstub_already_started.cpython-311.pyc new file mode 100644 index 0000000..2ffe591 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_initialstub_already_started.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_issue_515_freethread_gc.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_issue_515_freethread_gc.cpython-311.pyc new file mode 100644 index 0000000..7fd187f Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_issue_515_freethread_gc.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_slp_switch.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_slp_switch.cpython-311.pyc new file mode 100644 index 0000000..d79d2ce Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_slp_switch.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_critical_section.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_critical_section.cpython-311.pyc new file mode 100644 index 0000000..a1cf073 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_critical_section.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_three_greenlets.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_three_greenlets.cpython-311.pyc new file mode 100644 index 0000000..8f62c7a Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_three_greenlets.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_three_greenlets2.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_three_greenlets2.cpython-311.pyc new file mode 100644 index 0000000..e5f8143 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_three_greenlets2.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_two_greenlets.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_two_greenlets.cpython-311.pyc new file mode 100644 index 0000000..adadced Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/fail_switch_two_greenlets.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/leakcheck.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/leakcheck.cpython-311.pyc new file mode 100644 index 0000000..e4f3369 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/leakcheck.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_contextvars.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_contextvars.cpython-311.pyc new file mode 100644 index 0000000..009f796 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_contextvars.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_cpp.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_cpp.cpython-311.pyc new file mode 100644 index 0000000..36b29cd Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_cpp.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_extension_interface.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_extension_interface.cpython-311.pyc new file mode 100644 index 0000000..86b8656 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_extension_interface.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_gc.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_gc.cpython-311.pyc new file mode 100644 index 0000000..db6a06f Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_gc.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_generator.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_generator.cpython-311.pyc new file mode 100644 index 0000000..413bedc Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_generator.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_generator_nested.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_generator_nested.cpython-311.pyc new file mode 100644 index 0000000..cc6aec5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_generator_nested.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_greenlet.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_greenlet.cpython-311.pyc new file mode 100644 index 0000000..9c76492 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_greenlet.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_greenlet_trash.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_greenlet_trash.cpython-311.pyc new file mode 100644 index 0000000..a33f385 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_greenlet_trash.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_interpreter_shutdown.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_interpreter_shutdown.cpython-311.pyc new file mode 100644 index 0000000..571596c Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_interpreter_shutdown.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_leaks.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_leaks.cpython-311.pyc new file mode 100644 index 0000000..ecdff68 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_leaks.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_stack_saved.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_stack_saved.cpython-311.pyc new file mode 100644 index 0000000..19e1535 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_stack_saved.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_throw.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_throw.cpython-311.pyc new file mode 100644 index 0000000..056f1c6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_throw.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_tracing.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_tracing.cpython-311.pyc new file mode 100644 index 0000000..98d2ded Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_tracing.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_version.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_version.cpython-311.pyc new file mode 100644 index 0000000..037fcb4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_version.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_weakref.cpython-311.pyc b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_weakref.cpython-311.pyc new file mode 100644 index 0000000..01644a4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/__pycache__/test_weakref.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension.c b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension.c new file mode 100644 index 0000000..35a2dd2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension.c @@ -0,0 +1,279 @@ +/* This is a set of functions used by test_extension_interface.py to test the + * Greenlet C API. + */ + +#include "../greenlet.h" + +#ifndef Py_RETURN_NONE +# define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None +#endif + +#define TEST_MODULE_NAME "_test_extension" + +// CAUTION: MSVC is stupidly picky: +// +// "The compiler ignores, without warning, any __declspec keywords +// placed after * or & and in front of the variable identifier in a +// declaration." +// (https://docs.microsoft.com/en-us/cpp/cpp/declspec?view=msvc-160) +// +// So pointer return types must be handled differently (because of the +// trailing *), or you get inscrutable compiler warnings like "error +// C2059: syntax error: ''" +// +// In C23, there is a standard syntax for attributes, and +// GCC defines an attribute to use with this: [[gnu:noinline]]. +// In the future, this is expected to become standard. + +#if defined(__GNUC__) || defined(__clang__) +/* We used to check for GCC 4+ or 3.4+, but those compilers are + laughably out of date. Just assume they support it. */ +# define UNUSED(x) UNUSED_ ## x __attribute__((__unused__)) +#elif defined(_MSC_VER) +/* We used to check for && (_MSC_VER >= 1300) but that's also out of date. */ +# define UNUSED(x) UNUSED_ ## x +#endif + +static PyObject* +test_switch(PyObject* UNUSED(self), PyObject* greenlet) +{ + PyObject* result = NULL; + + if (greenlet == NULL || !PyGreenlet_Check(greenlet)) { + PyErr_BadArgument(); + return NULL; + } + + result = PyGreenlet_Switch((PyGreenlet*)greenlet, NULL, NULL); + if (result == NULL) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_AssertionError, + "greenlet.switch() failed for some reason."); + } + return NULL; + } + return result; +} + +static PyObject* +test_switch_kwargs(PyObject* UNUSED(self), PyObject* args, PyObject* kwargs) +{ + PyGreenlet* g = NULL; + PyObject* result = NULL; + + if (!PyArg_ParseTuple(args, "O!", &PyGreenlet_Type, &g)) { + return NULL; + } + + if (g == NULL || !PyGreenlet_Check(g)) { + PyErr_BadArgument(); + return NULL; + } + + result = PyGreenlet_Switch(g, NULL, kwargs); + if (result == NULL) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_AssertionError, + "greenlet.switch() failed for some reason."); + } + return NULL; + } + return result; +} + +static PyObject* +test_getcurrent(PyObject* UNUSED(self)) +{ + PyGreenlet* g = PyGreenlet_GetCurrent(); + if (g == NULL || !PyGreenlet_Check(g) || !PyGreenlet_ACTIVE(g)) { + PyErr_SetString(PyExc_AssertionError, + "getcurrent() returned an invalid greenlet"); + Py_XDECREF(g); + return NULL; + } + Py_DECREF(g); + Py_RETURN_NONE; +} + +static PyObject* +test_setparent(PyObject* UNUSED(self), PyObject* arg) +{ + PyGreenlet* current; + PyGreenlet* greenlet = NULL; + PyObject* switch_result = NULL; + + if (arg == NULL || !PyGreenlet_Check(arg)) { + PyErr_BadArgument(); + return NULL; + } + if ((current = PyGreenlet_GetCurrent()) == NULL) { + return NULL; + } + greenlet = (PyGreenlet*)arg; + if (PyGreenlet_SetParent(greenlet, current)) { + Py_DECREF(current); + return NULL; + } + Py_DECREF(current); + + switch_result = PyGreenlet_Switch(greenlet, NULL, NULL); + if (switch_result == NULL) { + return NULL; + } + Py_DECREF(switch_result); + Py_RETURN_NONE; +} + +static PyObject* +test_new_greenlet(PyObject* UNUSED(self), PyObject* callable) +{ + PyObject* result = NULL; + PyGreenlet* greenlet = PyGreenlet_New(callable, NULL); + + if (!greenlet) { + return NULL; + } + + result = PyGreenlet_Switch(greenlet, NULL, NULL); + Py_CLEAR(greenlet); + if (result == NULL) { + return NULL; + } + + return result; +} + +static PyObject* +test_raise_dead_greenlet(PyObject* UNUSED(self)) +{ + PyErr_SetString(PyExc_GreenletExit, "test GreenletExit exception."); + return NULL; +} + +static PyObject* +test_raise_greenlet_error(PyObject* UNUSED(self)) +{ + PyErr_SetString(PyExc_GreenletError, "test greenlet.error exception"); + return NULL; +} + +static PyObject* +test_throw(PyObject* UNUSED(self), PyGreenlet* g) +{ + const char msg[] = "take that sucka!"; + PyObject* msg_obj = Py_BuildValue("s", msg); + + if (msg_obj == NULL) { + return NULL; + } + + + PyGreenlet_Throw(g, PyExc_ValueError, msg_obj, NULL); + Py_DECREF(msg_obj); + if (PyErr_Occurred()) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject* +test_throw_exact(PyObject* UNUSED(self), PyObject* args) +{ + PyGreenlet* g = NULL; + PyObject* typ = NULL; + PyObject* val = NULL; + PyObject* tb = NULL; + + if (!PyArg_ParseTuple(args, "OOOO:throw", &g, &typ, &val, &tb)) { + return NULL; + } + + PyGreenlet_Throw(g, typ, val, tb); + if (PyErr_Occurred()) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject* +getcurrent_api(PyObject* UNUSED(self)) +{ + return (PyObject*)PyGreenlet_GetCurrent(); + +} + +static PyMethodDef test_methods[] = { + {"test_switch", + (PyCFunction)test_switch, + METH_O, + "Switch to the provided greenlet sending provided arguments, and \n" + "return the results."}, + {"test_switch_kwargs", + (PyCFunction)test_switch_kwargs, + METH_VARARGS | METH_KEYWORDS, + "Switch to the provided greenlet sending the provided keyword args."}, + {"test_getcurrent", + (PyCFunction)test_getcurrent, + METH_NOARGS, + "Test PyGreenlet_GetCurrent()"}, + {"test_setparent", + (PyCFunction)test_setparent, + METH_O, + "Se the parent of the provided greenlet and switch to it."}, + {"test_new_greenlet", + (PyCFunction)test_new_greenlet, + METH_O, + "Test PyGreenlet_New()"}, + {"test_raise_dead_greenlet", + (PyCFunction)test_raise_dead_greenlet, + METH_NOARGS, + "Just raise greenlet.GreenletExit"}, + {"test_raise_greenlet_error", + (PyCFunction)test_raise_greenlet_error, + METH_NOARGS, + "Just raise greenlet.error"}, + {"test_throw", + (PyCFunction)test_throw, + METH_O, + "Throw a ValueError at the provided greenlet"}, + {"test_throw_exact", + (PyCFunction)test_throw_exact, + METH_VARARGS, + "Throw exactly the arguments given at the provided greenlet"}, + { + "getcurrent_api", + (PyCFunction)getcurrent_api, + METH_NOARGS, + "Direct call to the PyGreenlet_GetCurrent API."}, + {NULL, NULL, 0, NULL} +}; + + +#define INITERROR return NULL + +static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT, + TEST_MODULE_NAME, + NULL, + 0, + test_methods, + NULL, + NULL, + NULL, + NULL}; + +PyMODINIT_FUNC +PyInit__test_extension(void) +{ + PyObject* module = NULL; + module = PyModule_Create(&moduledef); + + if (module == NULL) { + return NULL; + } + + PyGreenlet_Import(); +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); +#endif + return module; +} diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..41bb346 Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpp b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpp new file mode 100644 index 0000000..fc7902f --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpp @@ -0,0 +1,230 @@ +/* This is a set of functions used to test C++ exceptions are not + * broken during greenlet switches + */ + +#include "../greenlet.h" +#include "../greenlet_compiler_compat.hpp" +#include +#include + +struct exception_t { + int depth; + exception_t(int depth) : depth(depth) {} +}; + +/* Functions are called via pointers to prevent inlining */ +static void (*p_test_exception_throw_nonstd)(int depth); +static void (*p_test_exception_throw_std)(); +static PyObject* (*p_test_exception_switch_recurse)(int depth, int left); + +static void +test_exception_throw_nonstd(int depth) +{ + throw exception_t(depth); +} + +static void +test_exception_throw_std() +{ + throw std::runtime_error("Thrown from an extension."); +} + +static PyObject* +test_exception_switch_recurse(int depth, int left) +{ + if (left > 0) { + return p_test_exception_switch_recurse(depth, left - 1); + } + + PyObject* result = NULL; + PyGreenlet* self = PyGreenlet_GetCurrent(); + if (self == NULL) + return NULL; + + try { + if (PyGreenlet_Switch(PyGreenlet_GET_PARENT(self), NULL, NULL) == NULL) { + Py_DECREF(self); + return NULL; + } + p_test_exception_throw_nonstd(depth); + PyErr_SetString(PyExc_RuntimeError, + "throwing C++ exception didn't work"); + } + catch (const exception_t& e) { + if (e.depth != depth) + PyErr_SetString(PyExc_AssertionError, "depth mismatch"); + else + result = PyLong_FromLong(depth); + } + catch (...) { + PyErr_SetString(PyExc_RuntimeError, "unexpected C++ exception"); + } + + Py_DECREF(self); + return result; +} + +/* test_exception_switch(int depth) + * - recurses depth times + * - switches to parent inside try/catch block + * - throws an exception that (expected to be caught in the same function) + * - verifies depth matches (exceptions shouldn't be caught in other greenlets) + */ +static PyObject* +test_exception_switch(PyObject* UNUSED(self), PyObject* args) +{ + int depth; + if (!PyArg_ParseTuple(args, "i", &depth)) + return NULL; + return p_test_exception_switch_recurse(depth, depth); +} + + +static PyObject* +py_test_exception_throw_nonstd(PyObject* UNUSED(self), PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) + return NULL; + p_test_exception_throw_nonstd(0); + PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw"); + return NULL; +} + +static PyObject* +py_test_exception_throw_std(PyObject* UNUSED(self), PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) + return NULL; + p_test_exception_throw_std(); + PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw"); + return NULL; +} + +static PyObject* +py_test_call(PyObject* UNUSED(self), PyObject* arg) +{ + PyObject* noargs = PyTuple_New(0); + PyObject* ret = PyObject_Call(arg, noargs, nullptr); + Py_DECREF(noargs); + return ret; +} + + + +/* test_exception_switch_and_do_in_g2(g2func) + * - creates new greenlet g2 to run g2func + * - switches to g2 inside try/catch block + * - verifies that no exception has been caught + * + * it is used together with test_exception_throw to verify that unhandled + * exceptions thrown in one greenlet do not propagate to other greenlet nor + * segfault the process. + */ +static PyObject* +test_exception_switch_and_do_in_g2(PyObject* UNUSED(self), PyObject* args) +{ + PyObject* g2func = NULL; + PyObject* result = NULL; + + if (!PyArg_ParseTuple(args, "O", &g2func)) + return NULL; + PyGreenlet* g2 = PyGreenlet_New(g2func, NULL); + if (!g2) { + return NULL; + } + + try { + result = PyGreenlet_Switch(g2, NULL, NULL); + if (!result) { + Py_DECREF(g2); + return NULL; + } + } + catch (const exception_t& e) { + /* if we are here the memory can be already corrupted and the program + * might crash before below py-level exception might become printed. + * -> print something to stderr to make it clear that we had entered + * this catch block. + * See comments in inner_bootstrap() + */ +#if defined(WIN32) || defined(_WIN32) + fprintf(stderr, "C++ exception unexpectedly caught in g1\n"); + PyErr_SetString(PyExc_AssertionError, "C++ exception unexpectedly caught in g1"); + Py_XDECREF(result); + return NULL; +#else + throw; +#endif + } + + Py_XDECREF(result); + Py_RETURN_NONE; +} + +static PyMethodDef test_methods[] = { + {"test_exception_switch", + (PyCFunction)&test_exception_switch, + METH_VARARGS, + "Switches to parent twice, to test exception handling and greenlet " + "switching."}, + {"test_exception_switch_and_do_in_g2", + (PyCFunction)&test_exception_switch_and_do_in_g2, + METH_VARARGS, + "Creates new greenlet g2 to run g2func and switches to it inside try/catch " + "block. Used together with test_exception_throw to verify that unhandled " + "C++ exceptions thrown in a greenlet doe not corrupt memory."}, + {"test_exception_throw_nonstd", + (PyCFunction)&py_test_exception_throw_nonstd, + METH_VARARGS, + "Throws non-standard C++ exception. Calling this function directly should abort the process." + }, + {"test_exception_throw_std", + (PyCFunction)&py_test_exception_throw_std, + METH_VARARGS, + "Throws standard C++ exception. Calling this function directly should abort the process." + }, + {"test_call", + (PyCFunction)&py_test_call, + METH_O, + "Call the given callable. Unlike calling it directly, this creates a " + "new C-level stack frame, which may be helpful in testing." + }, + {NULL, NULL, 0, NULL} +}; + + +static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT, + "greenlet.tests._test_extension_cpp", + NULL, + 0, + test_methods, + NULL, + NULL, + NULL, + NULL}; + +PyMODINIT_FUNC +PyInit__test_extension_cpp(void) +{ + PyObject* module = NULL; + + module = PyModule_Create(&moduledef); + + if (module == NULL) { + return NULL; + } + + PyGreenlet_Import(); + if (_PyGreenlet_API == NULL) { + return NULL; + } + + p_test_exception_throw_nonstd = test_exception_throw_nonstd; + p_test_exception_throw_std = test_exception_throw_std; + p_test_exception_switch_recurse = test_exception_switch_recurse; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); +#endif + + return module; +} diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..3e9b92e Binary files /dev/null and b/venv/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_c_stack_refs_suspended_gc.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_c_stack_refs_suspended_gc.py new file mode 100644 index 0000000..43bf7bb --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_c_stack_refs_suspended_gc.py @@ -0,0 +1,85 @@ +"""Regression test for issue #515: GC of a suspended greenlet's C-stack refs. + +A greenlet that suspends mid attribute-resolution holds a ``_PyCStackRef`` to +the looked-up object on its C stack. The free-threaded collector only walks the +running thread's refs, so greenlet must visit a suspended greenlet's from +``tp_traverse``, or an object reachable only through them is freed early. + +The test pins a deferred-refcounted class (a metaclass ``__get__`` makes the +class act as a descriptor), switches away from inside ``__get__``, drops every +other reference, and collects. It passes only if the class is still alive and +the suspended greenlet is what keeps it alive. Prints "C STACK REFS GC OK" and +exits 0 on a fixed build, non-zero on a regressed one. +""" +import gc +import sys +import sysconfig +import weakref + +import greenlet + +# Only free-threaded builds have the C-stack-ref / deferred-refcount machinery. +FREETHREAD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + +parent = greenlet.getcurrent() +observed = {} + + +class Meta(type): + def __get__(cls, obj, objtype=None): + # ``cls`` is the class being resolved, pinned in a _PyCStackRef across + # this call. Drop the descriptor-protocol locals so a suspended frame + # can't keep the class alive on its own and mask the bug. + del cls, obj, objtype + child.switch() + return 42 + + +pinned = Meta('pinned', (), {}) # a class => deferred-refcounted +holder = type('holder', (), {'attr': pinned}) # holder.attr invokes Meta.__get__ +box = [pinned] +del pinned + + +def child_work(): + # parent is suspended inside Meta.__get__ holding a C-stack ref to the class. + # Drop every other reference, then collect. + ref = weakref.ref(box[0]) + box[0] = None + del holder.attr + for _ in range(5): + gc.collect() + cls = ref() + if cls is None: + observed['status'] = 'collected' + elif not FREETHREAD or any(r is parent for r in gc.get_referrers(cls)): + # Kept alive by the suspended greenlet (or, with the GIL, by refcounting). + observed['status'] = 'ok' + else: + # Alive, but not because of the greenlet: a masking reference hid the + # C-stack-ref path (the class's own mro/bases are a self-cycle, not one). + observed['status'] = 'masked' + own = (cls.__mro__, cls.__bases__) + observed['maskers'] = sorted( + {type(r).__name__ for r in gc.get_referrers(cls) + if r is not parent and r is not own[0] and r is not own[1]} + ) or [''] + del cls + parent.switch() + + +child = greenlet.greenlet(child_work) +result = holder().attr +assert result == 42, result + +status = observed.get('status') +print(f"py={sys.version.split()[0]} gil={getattr(sys, '_is_gil_enabled', lambda: True)()} " + f"greenlet={greenlet.__version__} status={status}", flush=True) +if status == 'collected': + raise SystemExit("REGRESSED: class reachable only through a suspended " + "greenlet's C-stack ref was collected early") +if status == 'masked': + raise SystemExit("REGRESSED: class stayed alive but not through the suspended " + "greenlet; a masking reference (%s) hid the C-stack-ref path" + % ', '.join(observed.get('maskers', ()))) +print("C STACK REFS GC OK", flush=True) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_clearing_run_switches.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_clearing_run_switches.py new file mode 100644 index 0000000..6dd1492 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_clearing_run_switches.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +If we have a run callable passed to the constructor or set as an +attribute, but we don't actually use that (because ``__getattribute__`` +or the like interferes), then when we clear callable before beginning +to run, there's an opportunity for Python code to run. + +""" +import greenlet + +g = None +main = greenlet.getcurrent() + +results = [] + +class RunCallable: + + def __del__(self): + results.append(('RunCallable', '__del__')) + main.switch('from RunCallable') + + +class G(greenlet.greenlet): + + def __getattribute__(self, name): + if name == 'run': + results.append(('G.__getattribute__', 'run')) + return run_func + return object.__getattribute__(self, name) + + +def run_func(): + results.append(('run_func', 'enter')) + + +g = G(RunCallable()) +# Try to start G. It will get to the point where it deletes +# its run callable C++ variable in inner_bootstrap. That triggers +# the __del__ method, which switches back to main before g +# actually even starts running. +x = g.switch() +results.append(('main: g.switch()', x)) +# In the C++ code, this results in g->g_switch() appearing to return, even though +# it has yet to run. +print('In main with', x, flush=True) +g.switch() +print('RESULTS', results) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py new file mode 100644 index 0000000..fa4dc2e --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Helper for testing a C++ exception throw aborts the process. + +Takes one argument, the name of the function in :mod:`_test_extension_cpp` to call. +""" +import sys +import greenlet +from greenlet.tests import _test_extension_cpp +print('fail_cpp_exception is running') + +def run_unhandled_exception_in_greenlet_aborts(): + def _(): + _test_extension_cpp.test_exception_switch_and_do_in_g2( + _test_extension_cpp.test_exception_throw_nonstd + ) + g1 = greenlet.greenlet(_) + g1.switch() + + +func_name = sys.argv[1] +try: + func = getattr(_test_extension_cpp, func_name) +except AttributeError: + if func_name == run_unhandled_exception_in_greenlet_aborts.__name__: + func = run_unhandled_exception_in_greenlet_aborts + elif func_name == 'run_as_greenlet_target': + g = greenlet.greenlet(_test_extension_cpp.test_exception_throw_std) + func = g.switch + else: + raise +print('raising', func, flush=True) +func() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py new file mode 100644 index 0000000..c1a44ef --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py @@ -0,0 +1,78 @@ +""" +Testing initialstub throwing an already started exception. +""" + +import greenlet + +a = None +b = None +c = None +main = greenlet.getcurrent() + +# If we switch into a dead greenlet, +# we go looking for its parents. +# if a parent is not yet started, we start it. + +results = [] + +def a_run(*args): + #results.append('A') + results.append(('Begin A', args)) + + +def c_run(): + results.append('Begin C') + b.switch('From C') + results.append('C done') + +class A(greenlet.greenlet): pass + +class B(greenlet.greenlet): + doing_it = False + def __getattribute__(self, name): + if name == 'run' and not self.doing_it: + assert greenlet.getcurrent() is c + self.doing_it = True + results.append('Switch to b from B.__getattribute__ in ' + + type(greenlet.getcurrent()).__name__) + b.switch() + results.append('B.__getattribute__ back from main in ' + + type(greenlet.getcurrent()).__name__) + if name == 'run': + name = '_B_run' + return object.__getattribute__(self, name) + + def _B_run(self, *arg): + results.append(('Begin B', arg)) + results.append('_B_run switching to main') + main.switch('From B') + +class C(greenlet.greenlet): + pass +a = A(a_run) +b = B(parent=a) +c = C(c_run, b) + +# Start a child; while running, it will start B, +# but starting B will ALSO start B. +result = c.switch() +results.append(('main from c', result)) + +# Switch back to C, which was in the middle of switching +# already. This will throw the ``GreenletStartedWhileInPython`` +# exception, which results in parent A getting started (B is finished) +c.switch() + +results.append(('A dead?', a.dead, 'B dead?', b.dead, 'C dead?', c.dead)) + +# A and B should both be dead now. +assert a.dead +assert b.dead +assert not c.dead + +result = c.switch() +results.append(('main from c.2', result)) +# Now C is dead +assert c.dead + +print("RESULTS:", results) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_issue_515_freethread_gc.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_issue_515_freethread_gc.py new file mode 100644 index 0000000..42b11be --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_issue_515_freethread_gc.py @@ -0,0 +1,112 @@ +"""Reproducer for the free-threaded GC crash in issue #515. + +https://github.com/python-greenlet/greenlet/issues/515 + +Before the fix, ``set_initial_state`` copied the parent thread state's +``_PyCStackRef`` list head into each new greenlet. Those nodes sit on the +parent greenlet's C stack, so once the child starts running on its own stack +and overwrites that region their ``next`` pointers dangle. The free-threaded +collector walks ``c_stack_refs`` for every thread, so a GC on any thread ends +up chasing those dangling nodes (inside ``gc_collect_main`` -> +``gc_visit_thread_stacks``) while such a child is active, and segfaults. + +The fiddly part is getting a *non-empty* list to inherit at all. The +interpreter only holds a ``_PyCStackRef`` transiently, while it is resolving an +attribute -- ``_Py_LoadAttr_StackRefSteal`` pins ``self`` across ``obj.attr()`` +-- so the greenlet has to be primed from inside that window. Priming it from a +property getter does exactly that; it is also the same deep-attribute context +Playwright's sync bridge happens to start its fiber from. A churn thread then +keeps the collector busy while the dispatcher runs on its stale list. + +A fixed build prints "ISSUE 515 OK"; a regressed one segfaults in about a +second. With-GIL builds are unaffected -- ordinary refcounting keeps the nodes +alive -- so there it simply prints the OK line. +""" +import gc +import os +import sys +import threading + +import greenlet + +# The crash lands within a second, so a few seconds leaves plenty of margin on +# a slow machine without dragging out a fixed build. +DURATION = float(os.environ.get("REPRO_SECONDS", "4")) +NWORKERS = int(os.environ.get("REPRO_THREADS", "6")) + + +def _busy(): + # Keep the dispatcher doing work (resolving str methods) so a collection on + # the churn thread catches it while it is the active greenlet. + return "abc".upper().lower().strip().title() + + +class Bridge: + def __init__(self, stop): + self.stop = stop + self.main = greenlet.getcurrent() + self.disp = greenlet.greenlet(self._loop) + + def _loop(self): + while not self.stop[0]: + for _ in range(50): + _busy() + self.main.switch() + + @property + def _prime(self): + # Reaching this getter means we are mid attribute-resolution, so a + # _PyCStackRef for ``self`` is live right now. Priming the dispatcher + # from here is what makes it inherit a non-empty c_stack_refs head. + self.disp.switch() + return None + + def start(self): + # Reading the property (rather than calling a method) is deliberate: the + # getter runs *during* attribute resolution, so it primes the dispatcher + # while the _PyCStackRef for ``self`` is still held. We return the value + # only so this isn't a bare, pointless-looking expression statement. + return self._prime + + def resume(self): + self.disp.switch() + + +def worker(stop): + bridge = Bridge(stop) + bridge.start() + # start() has returned, so the stack the inherited nodes point at is being + # reused -- the dispatcher's list is stale now. Keep it active to trip the GC. + while not stop[0]: + bridge.resume() + + +def churn(stop): + while not stop[0]: + # A cycle forces a real (stop-the-world) collection rather than just a + # refcount drop. + nodes = [{"i": i, "next": None} for i in range(500)] + for a, b in zip(nodes, nodes[1:]): + a["next"] = b + nodes[-1]["next"] = nodes[0] + del nodes + gc.collect() + + +def main(): + stop = [False] + threads = [threading.Thread(target=churn, args=(stop,), daemon=True)] + threads += [threading.Thread(target=worker, args=(stop,), daemon=True) + for _ in range(NWORKERS)] + for t in threads: + t.start() + threading.Event().wait(DURATION) + stop[0] = True + for t in threads: + t.join(timeout=5) + print("ISSUE 515 OK") + + +if __name__ == "__main__": + main() + sys.exit(0) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py new file mode 100644 index 0000000..0990526 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" +A test helper for seeing what happens when slp_switch() +fails. +""" +# pragma: no cover + +import greenlet + + +print('fail_slp_switch is running', flush=True) + +runs = [] +def func(): + runs.append(1) + greenlet.getcurrent().parent.switch() + runs.append(2) + greenlet.getcurrent().parent.switch() + runs.append(3) + +g = greenlet._greenlet.UnswitchableGreenlet(func) +g.switch() +assert runs == [1] +g.switch() +assert runs == [1, 2] +g.force_slp_switch_error = True + +# This should crash. +g.switch() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_critical_section.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_critical_section.py new file mode 100644 index 0000000..b8f2c32 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_critical_section.py @@ -0,0 +1,38 @@ +"""Out-of-process payload for the free-threaded switch/critical-section deadlock. + +See ``freethread_switch_deadlock.py`` in the repo root for the full write-up. +Before the fix, a greenlet switch swapped C stacks but left any held +``PyCriticalSection`` locks (tracked in ``tstate->critical_section``) locked. +asyncio's ``Task.__step`` holds such a lock on the running task across the step, +so switching into a child greenlet and touching that task from there blocked +forever. A fixed build (and any GIL-enabled build) prints the sentinel; a +regressed free-threaded build deadlocks, and the watchdog turns that hang into a +non-zero exit so the test fails loudly instead of stalling the whole suite. +""" +import asyncio +import faulthandler +import sys + +import greenlet + +# The deadlock is immediate when present; the generous timeout is only so a +# genuinely regressed build still exits on the slowest CI, never on a good one. +faulthandler.dump_traceback_later(15, exit=True) + + +async def main(): + task = asyncio.current_task() # its running __step holds a lock on `task` + + def in_child_fiber(): + # Fresh C stack; the task's lock is still held by the fiber we left. + # A regressed build never returns from this first call. + task.add_done_callback(lambda _: None) + task.remove_done_callback(lambda _: None) + + greenlet.greenlet(in_child_fiber).switch() + + +asyncio.run(main()) +faulthandler.cancel_dump_traceback_later() +print("SWITCH CS OK") +sys.exit(0) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py new file mode 100644 index 0000000..e151b19 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py @@ -0,0 +1,44 @@ +""" +Uses a trace function to switch greenlets at unexpected times. + +In the trace function, we switch from the current greenlet to another +greenlet, which switches +""" +import greenlet + +g1 = None +g2 = None + +switch_to_g2 = False + +def tracefunc(*args): + print('TRACE', *args) + global switch_to_g2 + if switch_to_g2: + switch_to_g2 = False + g2.switch() + print('\tLEAVE TRACE', *args) + +def g1_run(): + print('In g1_run') + global switch_to_g2 + switch_to_g2 = True + from_parent = greenlet.getcurrent().parent.switch() + print('Return to g1_run') + print('From parent', from_parent) + +def g2_run(): + #g1.switch() + greenlet.getcurrent().parent.switch() + +greenlet.settrace(tracefunc) + +g1 = greenlet.greenlet(g1_run) +g2 = greenlet.greenlet(g2_run) + +# This switch didn't actually finish! +# And if it did, it would raise TypeError +# because g1_run() doesn't take any arguments. +g1.switch(1) +print('Back in main') +g1.switch(2) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py new file mode 100644 index 0000000..1f6b66b --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py @@ -0,0 +1,55 @@ +""" +Like fail_switch_three_greenlets, but the call into g1_run would actually be +valid. +""" +import greenlet + +g1 = None +g2 = None + +switch_to_g2 = True + +results = [] + +def tracefunc(*args): + results.append(('trace', args[0])) + print('TRACE', *args) + global switch_to_g2 + if switch_to_g2: + switch_to_g2 = False + g2.switch('g2 from tracefunc') + print('\tLEAVE TRACE', *args) + +def g1_run(arg): + results.append(('g1 arg', arg)) + print('In g1_run') + from_parent = greenlet.getcurrent().parent.switch('from g1_run') + results.append(('g1 from parent', from_parent)) + return 'g1 done' + +def g2_run(arg): + #g1.switch() + results.append(('g2 arg', arg)) + parent = greenlet.getcurrent().parent.switch('from g2_run') + global switch_to_g2 + switch_to_g2 = False + results.append(('g2 from parent', parent)) + return 'g2 done' + + +greenlet.settrace(tracefunc) + +g1 = greenlet.greenlet(g1_run) +g2 = greenlet.greenlet(g2_run) + +x = g1.switch('g1 from main') +results.append(('main g1', x)) +print('Back in main', x) +x = g1.switch('g2 from main') +results.append(('main g2', x)) +print('back in amain again', x) +x = g1.switch('g1 from main 2') +results.append(('main g1.2', x)) +x = g2.switch() +results.append(('main g2.2', x)) +print("RESULTS:", results) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py new file mode 100644 index 0000000..3e52345 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py @@ -0,0 +1,41 @@ +""" +Uses a trace function to switch greenlets at unexpected times. + +In the trace function, we switch from the current greenlet to another +greenlet, which switches +""" +import greenlet + +g1 = None +g2 = None + +switch_to_g2 = False + +def tracefunc(*args): + print('TRACE', *args) + global switch_to_g2 + if switch_to_g2: + switch_to_g2 = False + g2.switch() + print('\tLEAVE TRACE', *args) + +def g1_run(): + print('In g1_run') + global switch_to_g2 + switch_to_g2 = True + greenlet.getcurrent().parent.switch() + print('Return to g1_run') + print('Falling off end of g1_run') + +def g2_run(): + g1.switch() + print('Falling off end of g2') + +greenlet.settrace(tracefunc) + +g1 = greenlet.greenlet(g1_run) +g2 = greenlet.greenlet(g2_run) + +g1.switch() +print('Falling off end of main') +g2.switch() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/leakcheck.py b/venv/lib/python3.11/site-packages/greenlet/tests/leakcheck.py new file mode 100644 index 0000000..a1037d4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/leakcheck.py @@ -0,0 +1,363 @@ +# Copyright (c) 2018 gevent community +# Copyright (c) 2021 greenlet community +# +# This was originally part of gevent's test suite. The main author +# (Jason Madden) vendored a copy of it into greenlet. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import os +import sys +import gc + +from functools import wraps +import unittest + + +import objgraph + +# graphviz 0.18 (Nov 7 2021), available only on Python 3.6 and newer, +# has added type hints (sigh). It wants to use ``typing.Literal`` for +# some stuff, but that's only available on Python 3.9+. If that's not +# found, it creates a ``unittest.mock.MagicMock`` object and annotates +# with that. These are GC'able objects, and doing almost *anything* +# with them results in an explosion of objects. For example, trying to +# compare them for equality creates new objects. This causes our +# leakchecks to fail, with reports like: +# +# greenlet.tests.leakcheck.LeakCheckError: refcount increased by [337, 1333, 343, 430, 530, 643, 769] +# _Call 1820 +546 +# dict 4094 +76 +# MagicProxy 585 +73 +# tuple 2693 +66 +# _CallList 24 +3 +# weakref 1441 +1 +# function 5996 +1 +# type 736 +1 +# cell 592 +1 +# MagicMock 8 +1 +# +# To avoid this, we *could* filter this type of object out early. In +# principle it could leak, but we don't use mocks in greenlet, so it +# doesn't leak from us. However, a further issue is that ``MagicMock`` +# objects have subobjects that are also GC'able, like ``_Call``, and +# those create new mocks of their own too. So we'd have to filter them +# as well, and they're not public. That's OK, we can workaround the +# problem by being very careful to never compare by equality or other +# user-defined operators, only using object identity or other builtin +# functions. + +RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS') +RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS +RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR') +RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR +RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX') +SKIP_LEAKCHECKS = RUNNING_ON_MANYLINUX or os.environ.get('GREENLET_SKIP_LEAKCHECKS') +SKIP_FAILING_LEAKCHECKS = os.environ.get('GREENLET_SKIP_FAILING_LEAKCHECKS') +ONLY_FAILING_LEAKCHECKS = os.environ.get('GREENLET_ONLY_FAILING_LEAKCHECKS') + +def ignores_leakcheck(func): + """ + Ignore the given object during leakchecks. + + Can be applied to a method, in which case the method will run, but + will not be subject to leak checks. + + If applied to a class, the entire class will be skipped during leakchecks. This + is intended to be used for classes that are very slow and cause problems such as + test timeouts; typically it will be used for classes that are subclasses of a base + class and specify variants of behaviour (such as pool sizes). + """ + func.ignore_leakcheck = True + return func + +def ignores_leakcheck_if(condition, message): + """ + Return a decorator that marks the function to be ignored during + leakchecks (see `ignores_leakcheck`) when *condition* is true. + + *message* describes why the leakcheck is ignored. When *condition* + is false, the function is returned unchanged. + """ + def decorator(func): + if condition: + func = ignores_leakcheck(func) + func.ignore_leakcheck_reason = message + return func + return decorator + +def fails_leakcheck(func): + """ + Mark that the function is known to leak. + """ + func.fails_leakcheck = True + if SKIP_FAILING_LEAKCHECKS: + func = unittest.skip("Skipping known failures")(func) + return func + +def fails_leakcheck_on_py314_or_less(func): + """ + Mark the function as known to leak (fails refcount leakchecks) on Python 3.14 or less. + """ + if sys.version_info[:2] <= (3, 14): + return fails_leakcheck(func) + return func + +class LeakCheckError(AssertionError): + pass + +if hasattr(sys, 'getobjects'): + # In a Python build with ``--with-trace-refs``, make objgraph + # trace *all* the objects, not just those that are tracked by the + # GC + class _MockGC(object): + def get_objects(self): + return sys.getobjects(0) # pylint:disable=no-member + def __getattr__(self, name): + return getattr(gc, name) + objgraph.gc = _MockGC() + fails_strict_leakcheck = fails_leakcheck +else: + def fails_strict_leakcheck(func): + """ + Decorator for a function that is known to fail when running + strict (``sys.getobjects()``) leakchecks. + + This type of leakcheck finds all objects, even those, such as + strings, which are not tracked by the garbage collector. + """ + return func + +class ignores_types_in_strict_leakcheck(object): + def __init__(self, types): + self.types = types + def __call__(self, func): + func.leakcheck_ignore_types = self.types + return func + +class _RefCountChecker(object): + + # Some builtin things that we ignore + # XXX: Those things were ignored by gevent, but they're important here, + # presumably. + IGNORED_TYPES = () #(tuple, dict, types.FrameType, types.TracebackType) + + # Names of types that should be ignored. Use this when we cannot + # or don't want to import the class directly. + IGNORED_TYPE_NAMES = ( + # This appears in Python3.14 with the JIT enabled. It + # doesn't seem to be directly exposed to Python; the only way to get + # one is to cause code to get jitted and then look for all objects + # and find one with this name. But they multiply as code + # executes and gets jitted, in ways we don't want to rely on. + # So just ignore it. + 'uop_executor', + ) + + def __init__(self, testcase, function): + self.testcase = testcase + self.function = function + self.deltas = [] + self.peak_stats = {} + self.ignored_types = () + + # The very first time we are called, we have already been + # self.setUp() by the test runner, so we don't need to do it again. + self.needs_setUp = False + + def _include_object_p(self, obj): + # pylint:disable=too-many-return-statements + # + # See the comment block at the top. We must be careful to + # avoid invoking user-defined operations. + if obj is self: + return False + kind = type(obj) + # ``self._include_object_p == obj`` returns NotImplemented + # for non-function objects, which causes the interpreter + # to try to reverse the order of arguments...which leads + # to the explosion of mock objects. We don't want that, so we implement + # the check manually. + if kind == type(self._include_object_p): # pylint: disable=unidiomatic-typecheck + try: + # pylint:disable=not-callable + exact_method_equals = self._include_object_p.__eq__(obj) + except AttributeError: + # Python 2.7 methods may only have __cmp__, and that raises a + # TypeError for non-method arguments + # pylint:disable=no-member + exact_method_equals = self._include_object_p.__cmp__(obj) == 0 + + if exact_method_equals is not NotImplemented and exact_method_equals: + return False + + # Similarly, we need to check identity in our __dict__ to avoid mock explosions. + for x in self.__dict__.values(): + if obj is x: + return False + + + if ( + kind in self.ignored_types + or kind in self.IGNORED_TYPES + or kind.__name__ in self.IGNORED_TYPE_NAMES + ): + return False + + + return True + + def _growth(self): + return objgraph.growth(limit=None, peak_stats=self.peak_stats, + filter=self._include_object_p) + + def _report_diff(self, growth): + if not growth: + return "" + + lines = [] + width = max(len(name) for name, _, _ in growth) + for name, count, delta in growth: + lines.append('%-*s%9d %+9d' % (width, name, count, delta)) + + diff = '\n'.join(lines) + return diff + + + def _run_test(self, args, kwargs): + gc_enabled = gc.isenabled() + gc.disable() + + if self.needs_setUp: + self.testcase.setUp() + self.testcase.skipTearDown = False + try: + self.function(self.testcase, *args, **kwargs) + finally: + self.testcase.tearDown() + self.testcase.doCleanups() + self.testcase.skipTearDown = True + self.needs_setUp = True + if gc_enabled: + gc.enable() + + def _growth_after(self): + # Grab post snapshot + # pylint:disable=no-member + if 'urlparse' in sys.modules: + sys.modules['urlparse'].clear_cache() + if 'urllib.parse' in sys.modules: + sys.modules['urllib.parse'].clear_cache() + + return self._growth() + + def _check_deltas(self, growth): + # Return false when we have decided there is no leak, + # true if we should keep looping, raises an assertion + # if we have decided there is a leak. + + deltas = self.deltas + if not deltas: + # We haven't run yet, no data, keep looping + return True + + if gc.garbage: + raise LeakCheckError("Generated uncollectable garbage %r" % (gc.garbage,)) + + + # the following configurations are classified as "no leak" + # [0, 0] + # [x, 0, 0] + # [... a, b, c, d] where a+b+c+d = 0 + # + # the following configurations are classified as "leak" + # [... z, z, z] where z > 0 + + if deltas[-2:] == [0, 0] and len(deltas) in (2, 3): + return False + + if deltas[-3:] == [0, 0, 0]: + return False + + if len(deltas) >= 4 and sum(deltas[-4:]) == 0: + return False + + if len(deltas) >= 3 and deltas[-1] > 0 and deltas[-1] == deltas[-2] and deltas[-2] == deltas[-3]: + diff = self._report_diff(growth) + raise LeakCheckError('refcount increased by %r\n%s' % (deltas, diff)) + + # OK, we don't know for sure yet. Let's search for more + if sum(deltas[-3:]) <= 0 or sum(deltas[-4:]) <= 0 or deltas[-4:].count(0) >= 2: + # this is suspicious, so give a few more runs + limit = 11 + else: + limit = 7 + if len(deltas) >= limit: + raise LeakCheckError('refcount increased by %r\n%s' + % (deltas, + self._report_diff(growth))) + + # We couldn't decide yet, keep going + return True + + def __call__(self, args, kwargs): + for _ in range(3): + gc.collect() + + expect_failure = getattr(self.function, 'fails_leakcheck', False) + if expect_failure: + self.testcase.expect_greenlet_leak = True + self.ignored_types = getattr(self.function, "leakcheck_ignore_types", ()) + + # Capture state before; the incremental will be + # updated by each call to _growth_after + growth = self._growth() + + try: + while self._check_deltas(growth): + self._run_test(args, kwargs) + + growth = self._growth_after() + + self.deltas.append(sum((stat[2] for stat in growth))) + except LeakCheckError: + if not expect_failure: + raise + else: + if expect_failure: + raise LeakCheckError("Expected %s to leak but it did not." % (self.function,)) + +def wrap_refcount(method): + if getattr(method, 'ignore_leakcheck', False) or SKIP_LEAKCHECKS: + reason = getattr(method, 'ignore_leakcheck_reason', None) + if reason and not SKIP_LEAKCHECKS: + print( + "Ignoring leakchecks for %s: %s" % (method.__name__, reason), + file=sys.stderr, + ) + return method + + @wraps(method) + def wrapper(self, *args, **kwargs): # pylint:disable=too-many-branches + if getattr(self, 'ignore_leakcheck', False): + raise unittest.SkipTest("This class ignored during leakchecks") + if ONLY_FAILING_LEAKCHECKS and not getattr(method, 'fails_leakcheck', False): + raise unittest.SkipTest("Only running tests that fail leakchecks.") + return _RefCountChecker(self, method)(args, kwargs) + + return wrapper diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py new file mode 100644 index 0000000..f7dea75 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py @@ -0,0 +1,282 @@ +import gc +import sys +import unittest +from contextvars import Context +from contextvars import ContextVar +from contextvars import copy_context +from functools import partial + +from greenlet import getcurrent +from greenlet import greenlet + +from . import PY314 +from . import TestCase + +# From the documentation: +# +# Important: Context Variables should be created at the top module +# level and never in closures. Context objects hold strong +# references to context variables which prevents context variables +# from being properly garbage collected. +ID_VAR = ContextVar("id", default=None) +VAR_VAR = ContextVar("var", default=None) +ContextVar = None + + +class ContextVarsTests(TestCase): + def _new_ctx_run(self, *args, **kwargs): + return copy_context().run(*args, **kwargs) + + def _increment(self, greenlet_id, callback, counts, expect): + ctx_var = ID_VAR + if expect is None: + self.assertIsNone(ctx_var.get()) + else: + self.assertEqual(ctx_var.get(), expect) + ctx_var.set(greenlet_id) + for _ in range(2): + counts[ctx_var.get()] += 1 + callback() + + def _test_context(self, propagate_by): + # pylint:disable=too-many-branches + ID_VAR.set(0) + + callback = getcurrent().switch + counts = dict((i, 0) for i in range(5)) + + lets = [ + greenlet(partial( + partial( + copy_context().run, + self._increment + ) if propagate_by == "run" else self._increment, + greenlet_id=i, + callback=callback, + counts=counts, + expect=( + i - 1 if propagate_by == "share" else + 0 if propagate_by in ("set", "run") else None + ) + )) + for i in range(1, 5) + ] + + for let in lets: + if propagate_by == "set": + let.gr_context = copy_context() + elif propagate_by == "share": + let.gr_context = getcurrent().gr_context + + for i in range(2): + counts[ID_VAR.get()] += 1 + for let in lets: + let.switch() + + if propagate_by == "run": + # Must leave each context.run() in reverse order of entry + for let in reversed(lets): + let.switch() + else: + # No context.run(), so fine to exit in any order. + for let in lets: + let.switch() + + for let in lets: + self.assertTrue(let.dead) + # When using run(), we leave the run() as the greenlet dies, + # and there's no context "underneath". When not using run(), + # gr_context still reflects the context the greenlet was + # running in. + if propagate_by == 'run': + self.assertIsNone(let.gr_context) + else: + self.assertIsNotNone(let.gr_context) + + + if propagate_by == "share": + self.assertEqual(counts, {0: 1, 1: 1, 2: 1, 3: 1, 4: 6}) + else: + self.assertEqual(set(counts.values()), set([2])) + + def test_context_propagated_by_context_run(self): + self._new_ctx_run(self._test_context, "run") + + def test_context_propagated_by_setting_attribute(self): + self._new_ctx_run(self._test_context, "set") + + def test_context_not_propagated(self): + self._new_ctx_run(self._test_context, None) + + def test_context_shared(self): + self._new_ctx_run(self._test_context, "share") + + def test_break_ctxvars(self): + let1 = greenlet(copy_context().run) + let2 = greenlet(copy_context().run) + let1.switch(getcurrent().switch) + let2.switch(getcurrent().switch) + # Since let2 entered the current context and let1 exits its own, the + # interpreter emits: + # RuntimeError: cannot exit context: thread state references a different context object + let1.switch() + + def test_not_broken_if_using_attribute_instead_of_context_run(self): + let1 = greenlet(getcurrent().switch) + let2 = greenlet(getcurrent().switch) + let1.gr_context = copy_context() + let2.gr_context = copy_context() + let1.switch() + let2.switch() + let1.switch() + let2.switch() + + def test_context_assignment_while_running(self): + # pylint:disable=too-many-statements + ID_VAR.set(None) + + def target(): + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + + # Context is created on first use + ID_VAR.set(1) + self.assertIsInstance(gr.gr_context, Context) + self.assertEqual(ID_VAR.get(), 1) + self.assertEqual(gr.gr_context[ID_VAR], 1) + + # Clearing the context makes it get re-created as another + # empty context when next used + old_context = gr.gr_context + gr.gr_context = None # assign None while running + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + ID_VAR.set(2) + self.assertIsInstance(gr.gr_context, Context) + self.assertEqual(ID_VAR.get(), 2) + self.assertEqual(gr.gr_context[ID_VAR], 2) + + new_context = gr.gr_context + getcurrent().parent.switch((old_context, new_context)) + # parent switches us back to old_context + + self.assertEqual(ID_VAR.get(), 1) + gr.gr_context = new_context # assign non-None while running + self.assertEqual(ID_VAR.get(), 2) + + getcurrent().parent.switch() + # parent switches us back to no context + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + gr.gr_context = old_context + self.assertEqual(ID_VAR.get(), 1) + + getcurrent().parent.switch() + # parent switches us back to no context + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + + gr = greenlet(target) + + with self.assertRaisesRegex(AttributeError, "can't delete context attribute"): + del gr.gr_context + + self.assertIsNone(gr.gr_context) + old_context, new_context = gr.switch() + self.assertIs(new_context, gr.gr_context) + self.assertEqual(old_context[ID_VAR], 1) + self.assertEqual(new_context[ID_VAR], 2) + self.assertEqual(new_context.run(ID_VAR.get), 2) + gr.gr_context = old_context # assign non-None while suspended + gr.switch() + self.assertIs(gr.gr_context, new_context) + gr.gr_context = None # assign None while suspended + gr.switch() + self.assertIs(gr.gr_context, old_context) + gr.gr_context = None + gr.switch() + self.assertIsNone(gr.gr_context) + + # Make sure there are no reference leaks + gr = None + gc.collect() + # Python 3.14 elides reference counting operations + # in some cases. See https://github.com/python/cpython/pull/130708 + self.assertEqual(sys.getrefcount(old_context), 2 if not PY314 else 1) + self.assertEqual(sys.getrefcount(new_context), 2 if not PY314 else 1) + + def test_context_assignment_different_thread(self): + import threading + VAR_VAR.set(None) + ctx = Context() + + is_running = threading.Event() + should_suspend = threading.Event() + did_suspend = threading.Event() + should_exit = threading.Event() + holder = [] + + def greenlet_in_thread_fn(): + VAR_VAR.set(1) + is_running.set() + should_suspend.wait(10) + VAR_VAR.set(2) + getcurrent().parent.switch() + holder.append(VAR_VAR.get()) + + def thread_fn(): + gr = greenlet(greenlet_in_thread_fn) + gr.gr_context = ctx + holder.append(gr) + gr.switch() + did_suspend.set() + should_exit.wait(10) + gr.switch() + del gr + greenlet() # trigger cleanup + + thread = threading.Thread(target=thread_fn, daemon=True) + thread.start() + is_running.wait(10) + gr = holder[0] + + # Can't access or modify context if the greenlet is running + # in a different thread + with self.assertRaisesRegex(ValueError, "running in a different"): + getattr(gr, 'gr_context') + with self.assertRaisesRegex(ValueError, "running in a different"): + gr.gr_context = None + + should_suspend.set() + did_suspend.wait(10) + + # OK to access and modify context if greenlet is suspended + self.assertIs(gr.gr_context, ctx) + self.assertEqual(gr.gr_context[VAR_VAR], 2) + gr.gr_context = None + + should_exit.set() + thread.join(10) + + self.assertEqual(holder, [gr, None]) + + # Context can still be accessed/modified when greenlet is dead: + self.assertIsNone(gr.gr_context) + gr.gr_context = ctx + self.assertIs(gr.gr_context, ctx) + + # Otherwise we leak greenlets on some platforms. + # XXX: Should be able to do this automatically + del holder[:] + gr = None + thread = None + + def test_context_assignment_wrong_type(self): + g = greenlet() + with self.assertRaisesRegex(TypeError, + "greenlet context must be a contextvars.Context or None"): + g.gr_context = self + + +if __name__ == '__main__': + unittest.main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_cpp.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_cpp.py new file mode 100644 index 0000000..6ad5bcc --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_cpp.py @@ -0,0 +1,91 @@ +import gc +import subprocess +import unittest + +import greenlet +import objgraph + +from . import WIN +from . import TestCase +from . import _test_extension_cpp + + +class CPPTests(TestCase): + def test_exception_switch(self): + greenlets = [] + for i in range(4): + g = greenlet.greenlet(_test_extension_cpp.test_exception_switch) + g.switch(i) + greenlets.append(g) + for i, g in enumerate(greenlets): + self.assertEqual(g.switch(), i) + + def _do_test_unhandled_exception(self, target): + import os + import sys + script = os.path.join( + os.path.dirname(__file__), + 'fail_cpp_exception.py', + ) + args = [sys.executable, script, target.__name__ if not isinstance(target, str) else target] + __traceback_info__ = args + with self.assertRaises(subprocess.CalledProcessError) as exc: + subprocess.check_output( + args, + encoding='utf-8', + stderr=subprocess.STDOUT + ) + + ex = exc.exception + expected_exit = self.get_expected_returncodes_for_aborted_process() + self.assertIn(ex.returncode, expected_exit) + self.assertIn('fail_cpp_exception is running', ex.output) + return ex.output + + + def test_unhandled_nonstd_exception_aborts(self): + # verify that plain unhandled throw aborts + self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_nonstd) + + def test_unhandled_std_exception_aborts(self): + # verify that plain unhandled throw aborts + self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_std) + + @unittest.skipIf(WIN, "XXX: This does not crash on Windows") + # Meaning the exception is getting lost somewhere... + def test_unhandled_std_exception_as_greenlet_function_aborts(self): + # verify that plain unhandled throw aborts + output = self._do_test_unhandled_exception('run_as_greenlet_target') + self.assertIn( + # We really expect this to be prefixed with "greenlet: Unhandled C++ exception:" + # as added by our handler for std::exception (see TUserGreenlet.cpp), but + # that's not correct everywhere --- our handler never runs before std::terminate + # gets called (for example, on arm32). + 'Thrown from an extension.', + output + ) + + def test_unhandled_exception_in_greenlet_aborts(self): + # verify that unhandled throw called in greenlet aborts too + self._do_test_unhandled_exception('run_unhandled_exception_in_greenlet_aborts') + + + def test_leak_test_exception_switch_and_do_in_g2(self): + def raiser(): + raise ValueError("boom") + + gc.collect() + before = objgraph.count("greenlet") + + for _ in range(1000): + with self.assertRaises(ValueError): + _test_extension_cpp.test_exception_switch_and_do_in_g2(raiser) + + gc.collect() + after = objgraph.count("greenlet") + leaked = after - before + self.assertEqual(0, leaked) + + +if __name__ == '__main__': + unittest.main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py new file mode 100644 index 0000000..4261897 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py @@ -0,0 +1,141 @@ +import sys + +import greenlet +from . import _test_extension +from . import TestCase +from .leakcheck import ignores_leakcheck + +# pylint:disable=c-extension-no-member + +class CAPITests(TestCase): + def test_switch(self): + self.assertEqual( + 50, _test_extension.test_switch(greenlet.greenlet(lambda: 50))) + + def test_switch_kwargs(self): + def adder(x, y): + return x * y + g = greenlet.greenlet(adder) + self.assertEqual(6, _test_extension.test_switch_kwargs(g, x=3, y=2)) + + with self.assertRaisesRegex(TypeError, "argument 1 must be greenlet"): + _test_extension.test_switch_kwargs("not a greenlet") + + def test_setparent(self): + # pylint:disable=disallowed-name + def foo(): + def bar(): + greenlet.getcurrent().parent.switch() + + # This final switch should go back to the main greenlet, since + # the test_setparent() function in the C extension should have + # reparented this greenlet. + greenlet.getcurrent().parent.switch() + raise AssertionError("Should never have reached this code") + child = greenlet.greenlet(bar) + child.switch() + greenlet.getcurrent().parent.switch(child) + greenlet.getcurrent().parent.throw( + AssertionError("Should never reach this code")) + foo_child = greenlet.greenlet(foo).switch() + self.assertEqual(None, _test_extension.test_setparent(foo_child)) + + def test_getcurrent(self): + _test_extension.test_getcurrent() + + def test_new_greenlet(self): + self.assertEqual(-15, _test_extension.test_new_greenlet(lambda: -15)) + + def test_raise_greenlet_dead(self): + self.assertRaises( + greenlet.GreenletExit, _test_extension.test_raise_dead_greenlet) + + def test_raise_greenlet_error(self): + self.assertRaises( + greenlet.error, _test_extension.test_raise_greenlet_error) + + def test_throw(self): + seen = [] + + def foo(): # pylint:disable=disallowed-name + try: + greenlet.getcurrent().parent.switch() + except ValueError: + seen.append(sys.exc_info()[1]) + except greenlet.GreenletExit: + raise AssertionError + g = greenlet.greenlet(foo) + g.switch() + _test_extension.test_throw(g) + self.assertEqual(len(seen), 1) + self.assertTrue( + isinstance(seen[0], ValueError), + "ValueError was not raised in foo()") + self.assertEqual( + str(seen[0]), + 'take that sucka!', + "message doesn't match") + + def test_non_traceback_param(self): + with self.assertRaises(TypeError) as exc: + _test_extension.test_throw_exact( + greenlet.getcurrent(), + Exception, + Exception(), + self + ) + self.assertEqual(str(exc.exception), + "throw() third argument must be a traceback object") + + def test_instance_of_wrong_type(self): + with self.assertRaises(TypeError) as exc: + _test_extension.test_throw_exact( + greenlet.getcurrent(), + Exception(), + BaseException(), + None, + ) + + self.assertEqual(str(exc.exception), + "instance exception may not have a separate value") + + def test_not_throwable(self): + with self.assertRaises(TypeError) as exc: + _test_extension.test_throw_exact( + greenlet.getcurrent(), + "abc", + None, + None, + ) + self.assertEqual(str(exc.exception), + "exceptions must be classes, or instances, not str") + + @ignores_leakcheck + def test_leaks(self): + from . import PY314 + iters = 100 + if PY314: + expected_refs = [1] * iters + else: + expected_refs = [2] * iters + for name, caller in ( + ("test_switch", + lambda: _test_extension.test_switch(greenlet.greenlet(object))), + ("test_switch_kwargs", + lambda: _test_extension.test_switch_kwargs(greenlet.greenlet(object))), + ("test_new_greenlet", + lambda: _test_extension.test_new_greenlet(object)), + ): + with self.subTest(name): + results = [caller() for _ in range(iters)] + refs = [ + sys.getrefcount(i) - 1 # ignore ref in ``i`` + for i + in results + ] + self.assertEqual(refs, expected_refs) + + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_gc.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_gc.py new file mode 100644 index 0000000..fe075ba --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_gc.py @@ -0,0 +1,159 @@ +import gc + +import weakref +import sys +import greenlet + + +from . import TestCase +from . import RUNNING_ON_FREETHREAD_BUILD +from .leakcheck import fails_leakcheck_on_py314_or_less +# These only work with greenlet gc support +# which is no longer optional. +assert greenlet.GREENLET_USE_GC + +class TestGC(TestCase): + def test_dead_circular_ref(self): + o = weakref.ref(greenlet.greenlet(greenlet.getcurrent).switch()) + gc.collect() + if o() is not None: + print("O IS NOT NONE.", sys.getrefcount(o())) + self.assertIsNone(o()) + self.assertFalse(gc.garbage, gc.garbage) + + def test_circular_greenlet(self): + class circular_greenlet(greenlet.greenlet): + self = None + o = circular_greenlet() + o.self = o + o = weakref.ref(o) + gc.collect() + self.assertIsNone(o()) + self.assertFalse(gc.garbage, gc.garbage) + + def test_inactive_ref(self): + class inactive_greenlet(greenlet.greenlet): + def __init__(self): + greenlet.greenlet.__init__(self, run=self.run) + + def run(self): + pass + o = inactive_greenlet() + o = weakref.ref(o) + gc.collect() + self.assertIsNone(o()) + self.assertFalse(gc.garbage, gc.garbage) + + @fails_leakcheck_on_py314_or_less + def test_finalizer_crash(self): + # This test is designed to crash when active greenlets + # are made garbage collectable, until the underlying + # problem is resolved. How does it work: + # - order of object creation is important + # - array is created first, so it is moved to unreachable first + # - we create a cycle between a greenlet and this array + # - we create an object that participates in gc, is only + # referenced by a greenlet, and would corrupt gc lists + # on destruction, the easiest is to use an object with + # a finalizer + # - because array is the first object in unreachable it is + # cleared first, which causes all references to greenlet + # to disappear and causes greenlet to be destroyed, but since + # it is still live it causes a switch during gc, which causes + # an object with finalizer to be destroyed, which causes stack + # corruption and then a crash + + class object_with_finalizer(object): + def __del__(self): + pass + array = [] + parent = greenlet.getcurrent() + def greenlet_body(): + greenlet.getcurrent().object = object_with_finalizer() + try: + parent.switch() + except greenlet.GreenletExit: + print("Got greenlet exit!") + finally: + del greenlet.getcurrent().object + g = greenlet.greenlet(greenlet_body) + g.array = array + array.append(g) + g.switch() + del array + del g + greenlet.getcurrent() + gc.collect() + + def test_issue515_freethread_c_stack_refs(self): + # Guards issue #515: a new greenlet inherited the parent's C-stack refs + # and the free-threaded collector segfaulted following the dangling + # nodes. This has to run out of process because the regression is a hard + # crash, not something we can catch. + # https://github.com/python-greenlet/greenlet/issues/515 + if not RUNNING_ON_FREETHREAD_BUILD: + self.skipTest("Only free-threaded builds are affected") + output = self.run_script('fail_issue_515_freethread_gc.py') + self.assertIn('ISSUE 515 OK', output) + + def test_c_stack_refs_suspended_gc(self): + # Issue #515: a greenlet suspended while holding a _PyCStackRef must have + # those refs visited by tp_traverse, or the free-threaded collector frees + # an object reachable only through the suspended C stack. Runs the repro + # out of process. https://github.com/python-greenlet/greenlet/issues/515 + if not RUNNING_ON_FREETHREAD_BUILD: + self.skipTest("Only free-threaded builds are affected") + output = self.run_script('fail_c_stack_refs_suspended_gc.py') + self.assertIn('C STACK REFS GC OK', output) + + def test_crashing_deferred_object(self): + if sys.version_info < (3, 15): + self.skipTest("Test is 3.15+ only") + import doctest + def with_doctest(): + """ + >>> import gc + >>> from greenlet import getcurrent, greenlet, GreenletExit + >>> def outer(): + ... gc.collect() + >>> outer_glet = greenlet(outer) + >>> outer_glet.switch() + """ + doctest.run_docstring_examples(with_doctest, dict()) + + def test_cycle_in_suspended_frame(self): + if sys.version_info < (3, 15): + self.skipTest("Test is 3.15+ only") + import doctest + def with_doctest(): + """ + >>> import gc + >>> from greenlet import getcurrent, greenlet + >>> class Cycle: + ... def __del__(self): + ... print("(Running finalizer)") + >>> def collect_it(): + ... print("Collecting garbage") + ... gc.collect() + >>> def inner(): + ... cycle1 = Cycle() + ... cycle2 = Cycle() + ... cycle1.cycle = cycle2 + ... cycle2.cycle = cycle1 + ... getcurrent().parent.switch() + >>> def outer(): + ... glet = greenlet(inner) + ... glet.switch() + ... collect_it() + + >>> outer_glet = greenlet(outer) + >>> outer_glet.switch() + Collecting garbage + >>> outer_glet.dead + True + >>> collect_it() + Collecting garbage + (Running finalizer) + (Running finalizer) + """ + doctest.run_docstring_examples(with_doctest, dict()) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_generator.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_generator.py new file mode 100644 index 0000000..ca4a644 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_generator.py @@ -0,0 +1,59 @@ + +from greenlet import greenlet + +from . import TestCase + +class genlet(greenlet): + parent = None + def __init__(self, *args, **kwds): + self.args = args + self.kwds = kwds + + def run(self): + fn, = self.fn + fn(*self.args, **self.kwds) + + def __iter__(self): + return self + + def __next__(self): + self.parent = greenlet.getcurrent() + result = self.switch() + if self: + return result + + raise StopIteration + + next = __next__ + + +def Yield(value): + g = greenlet.getcurrent() + while not isinstance(g, genlet): + if g is None: + raise RuntimeError('yield outside a genlet') + g = g.parent + g.parent.switch(value) + + +def generator(func): + class Generator(genlet): + fn = (func,) + return Generator + +# ____________________________________________________________ + + +class GeneratorTests(TestCase): + def test_generator(self): + seen = [] + + def g(n): + for i in range(n): + seen.append(i) + Yield(i) + g = generator(g) + for _ in range(3): + for j in g(5): + seen.append(j) + self.assertEqual(seen, 3 * [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py new file mode 100644 index 0000000..8d752a6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py @@ -0,0 +1,168 @@ + +from greenlet import greenlet +from . import TestCase +from .leakcheck import fails_leakcheck + +class genlet(greenlet): + parent = None + def __init__(self, *args, **kwds): + self.args = args + self.kwds = kwds + self.child = None + + def run(self): + # Note the function is packed in a tuple + # to avoid creating a bound method for it. + fn, = self.fn + fn(*self.args, **self.kwds) + + def __iter__(self): + return self + + def set_child(self, child): + self.child = child + + def __next__(self): + if self.child: + child = self.child + while child.child: + tmp = child + child = child.child + tmp.child = None + + result = child.switch() + else: + self.parent = greenlet.getcurrent() + result = self.switch() + + if self: + return result + + raise StopIteration + + next = __next__ + +def Yield(value, level=1): + g = greenlet.getcurrent() + + while level != 0: + if not isinstance(g, genlet): + raise RuntimeError('yield outside a genlet') + if level > 1: + g.parent.set_child(g) + g = g.parent + level -= 1 + + g.switch(value) + + +def Genlet(func): + class TheGenlet(genlet): + fn = (func,) + return TheGenlet + +# ____________________________________________________________ + + +def g1(n, seen): + for i in range(n): + seen.append(i + 1) + yield i + + +def g2(n, seen): + for i in range(n): + seen.append(i + 1) + Yield(i) + +g2 = Genlet(g2) + + +def nested(i): + Yield(i) + + +def g3(n, seen): + for i in range(n): + seen.append(i + 1) + nested(i) +g3 = Genlet(g3) + + +def a(n): + if n == 0: + return + for ii in ax(n - 1): + Yield(ii) + Yield(n) +ax = Genlet(a) + + +def perms(l): + if len(l) > 1: + for e in l: + # No syntactical sugar for generator expressions + x = [Yield([e] + p) for p in perms([x for x in l if x != e])] + assert x + else: + Yield(l) +perms = Genlet(perms) + + +def gr1(n): + for ii in range(1, n): + Yield(ii) + Yield(ii * ii, 2) + +gr1 = Genlet(gr1) + + +def gr2(n, seen): + for ii in gr1(n): + seen.append(ii) + +gr2 = Genlet(gr2) + + +class NestedGeneratorTests(TestCase): + def test_layered_genlets(self): + seen = [] + for ii in gr2(5, seen): + seen.append(ii) + self.assertEqual(seen, [1, 1, 2, 4, 3, 9, 4, 16]) + + @fails_leakcheck + def test_permutations(self): + gen_perms = perms(list(range(4))) + permutations = list(gen_perms) + self.assertEqual(len(permutations), 4 * 3 * 2 * 1) + self.assertIn([0, 1, 2, 3], permutations) + self.assertIn([3, 2, 1, 0], permutations) + res = [] + for ii in zip(perms(list(range(4))), perms(list(range(3)))): + res.append(ii) + self.assertEqual( + res, + [([0, 1, 2, 3], [0, 1, 2]), ([0, 1, 3, 2], [0, 2, 1]), + ([0, 2, 1, 3], [1, 0, 2]), ([0, 2, 3, 1], [1, 2, 0]), + ([0, 3, 1, 2], [2, 0, 1]), ([0, 3, 2, 1], [2, 1, 0])]) + # XXX Test to make sure we are working as a generator expression + + def test_genlet_simple(self): + for g in g1, g2, g3: + seen = [] + for _ in range(3): + for j in g(5, seen): + seen.append(j) + self.assertEqual(seen, 3 * [1, 0, 2, 1, 3, 2, 4, 3, 5, 4]) + + def test_genlet_bad(self): + try: + Yield(10) + except RuntimeError: + pass + + def test_nested_genlets(self): + seen = [] + for ii in ax(5): + seen.append(ii) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py new file mode 100644 index 0000000..1a70168 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py @@ -0,0 +1,1496 @@ +import gc +import sys +import time +import threading +import unittest + +from abc import ABCMeta +from abc import abstractmethod + +import greenlet +from greenlet import greenlet as RawGreenlet +from . import TestCase +from . import RUNNING_ON_MANYLINUX +from . import PY313 +from . import PY314 +from . import WIN +from . import RUNNING_ON_FREETHREAD_BUILD +from .leakcheck import fails_leakcheck +from .leakcheck import fails_leakcheck_on_py314_or_less +from .leakcheck import ignores_leakcheck +from .leakcheck import ignores_leakcheck_if + + +# We manually manage locks in many tests +# pylint:disable=consider-using-with +# pylint:disable=too-many-public-methods +# This module is quite large. +# TODO: Refactor into separate test files. For example, +# put all the regression tests that used to produce +# crashes in test_greenlet_no_crash; put tests that DO deliberately crash +# the interpreter into test_greenlet_crash. +# pylint:disable=too-many-lines + +class SomeError(Exception): + pass + + +def fmain(seen): + try: + greenlet.getcurrent().parent.switch() + except: + seen.append(sys.exc_info()[0]) + raise + raise SomeError + + +def send_exception(g, exc): + # note: send_exception(g, exc) can be now done with g.throw(exc). + # the purpose of this test is to explicitly check the propagation rules. + def crasher(exc): + raise exc + g1 = RawGreenlet(crasher, parent=g) + g1.switch(exc) + + +class TestGreenlet(TestCase): + + def _do_simple_test(self): + lst = [] + + def f(): + lst.append(1) + greenlet.getcurrent().parent.switch() + lst.append(3) + g = RawGreenlet(f) + lst.append(0) + g.switch() + lst.append(2) + g.switch() + lst.append(4) + self.assertEqual(lst, list(range(5))) + + def test_simple(self): + self._do_simple_test() + + def test_switch_no_run_raises_AttributeError(self): + g = RawGreenlet() + with self.assertRaises(AttributeError) as exc: + g.switch() + + self.assertIn("run", str(exc.exception)) + + def test_throw_no_run_raises_AttributeError(self): + g = RawGreenlet() + with self.assertRaises(AttributeError) as exc: + g.throw(SomeError) + + self.assertIn("run", str(exc.exception)) + + def test_parent_equals_None(self): + g = RawGreenlet(parent=None) + self.assertIsNotNone(g) + self.assertIs(g.parent, greenlet.getcurrent()) + + def test_run_equals_None(self): + g = RawGreenlet(run=None) + self.assertIsNotNone(g) + self.assertIsNone(g.run) + + def test_two_children(self): + lst = [] + + def f(): + lst.append(1) + greenlet.getcurrent().parent.switch() + lst.extend([1, 1]) + g = RawGreenlet(f) + h = RawGreenlet(f) + g.switch() + self.assertEqual(len(lst), 1) + h.switch() + self.assertEqual(len(lst), 2) + h.switch() + self.assertEqual(len(lst), 4) + self.assertEqual(h.dead, True) + g.switch() + self.assertEqual(len(lst), 6) + self.assertEqual(g.dead, True) + + def test_two_recursive_children(self): + lst = [] + + def f(): + lst.append('b') + greenlet.getcurrent().parent.switch() + + def g(): + lst.append('a') + g = RawGreenlet(f) + g.switch() + lst.append('c') + self.assertEqual(sys.getrefcount(g), 2 if not PY314 else 1) + g = RawGreenlet(g) + # Python 3.14 elides reference counting operations + # in some cases. See https://github.com/python/cpython/pull/130708 + self.assertEqual(sys.getrefcount(g), 2 if not PY314 else 1) + g.switch() + self.assertEqual(lst, ['a', 'b', 'c']) + # Just the one in this frame, plus the one on the stack we pass to the function + self.assertEqual(sys.getrefcount(g), 2 if not PY314 else 1) + + def test_threads(self): + success = [] + + def f(): + self._do_simple_test() + success.append(True) + ths = [threading.Thread(target=f) for i in range(10)] + for th in ths: + th.start() + for th in ths: + th.join(10) + self.assertEqual(len(success), len(ths)) + + @ignores_leakcheck + def test_switching_many_threads(self): + # This can expose issues on the free-threaded build. + def creates_greenlet(greenlets, wait_to_die_until): + def body(): + while True: + greenlet.getcurrent().parent.switch() + g = greenlet.greenlet(body) + g.switch() + greenlets.append(g) + wait_to_die_until.wait() + + def switches_from_other_thread(greenlets, wait_to_check_until, quit_after): + wait_to_check_until.wait() + while not quit_after.is_set(): + for g in list(greenlets): + try: + if not g.dead: + g.switch() + except greenlet.error: + # Every attempt where the greenlet isn't dead should + # raise this + pass + + def run_it(thread_count=16): + greenlets = [] + greenlets_all_created = threading.Event() + all_threads_dead = threading.Event() + + creators = [ + threading.Thread(target=creates_greenlet, args=(greenlets, + greenlets_all_created)) + for _ + in range(thread_count) + ] + + switchers = [ + threading.Thread(target=switches_from_other_thread, + args=(greenlets, greenlets_all_created, + all_threads_dead)) + for _ + in range(thread_count) + ] + + for t in (creators + switchers): + t.start() + + # Simple polling loop + while len(greenlets) < thread_count: + time.sleep(0.0001) + greenlets_all_created.set() + + for t in creators: + t.join(1.0) + all_threads_dead.set() + for t in switchers: + t.join(1.0) + + # enough reps to have a chance of repeating, not so many it + # takes forever + for _ in range(20): + run_it() + gc.collect() + + def test_exception(self): + seen = [] + g1 = RawGreenlet(fmain) + g2 = RawGreenlet(fmain) + g1.switch(seen) + g2.switch(seen) + g2.parent = g1 + + self.assertEqual(seen, []) + #with self.assertRaises(SomeError): + # p("***Switching back") + # g2.switch() + # Creating this as a bound method can reveal bugs that + # are hidden on newer versions of Python that avoid creating + # bound methods for direct expressions; IOW, don't use the `with` + # form! + self.assertRaises(SomeError, g2.switch) + self.assertEqual(seen, [SomeError]) + + value = g2.switch() + self.assertEqual(value, ()) + self.assertEqual(seen, [SomeError]) + + value = g2.switch(25) + self.assertEqual(value, 25) + self.assertEqual(seen, [SomeError]) + + + def test_send_exception(self): + seen = [] + g1 = RawGreenlet(fmain) + g1.switch(seen) + self.assertRaises(KeyError, send_exception, g1, KeyError) + self.assertEqual(seen, [KeyError]) + + def test_dealloc(self): + seen = [] + g1 = RawGreenlet(fmain) + g2 = RawGreenlet(fmain) + g1.switch(seen) + g2.switch(seen) + self.assertEqual(seen, []) + del g1 + gc.collect() + self.assertEqual(seen, [greenlet.GreenletExit]) + del g2 + gc.collect() + self.assertEqual(seen, [greenlet.GreenletExit, greenlet.GreenletExit]) + + def test_dealloc_catches_GreenletExit_throws_other(self): + def run(): + try: + greenlet.getcurrent().parent.switch() + except greenlet.GreenletExit: + raise SomeError from None + + g = RawGreenlet(run) + g.switch() + + unraisable_events = [] + old_hook = sys.unraisablehook + def _capture(unraisable): + unraisable_events.append(unraisable) + sys.unraisablehook = _capture + try: + del g + finally: + sys.unraisablehook = old_hook + + self.assertEqual(len(unraisable_events), 1) + self.assertIsInstance(unraisable_events[0].exc_value, SomeError) + + + @unittest.skipIf( + PY313 and RUNNING_ON_MANYLINUX, + "Sometimes flaky (getting one GreenletExit in the second list)" + # Probably due to funky timing interactions? + # TODO: FIXME Make that work. + ) + + def test_dealloc_other_thread(self): + seen = [] + someref = [] + + bg_glet_created_running_and_no_longer_ref_in_bg = threading.Event() + fg_ref_released = threading.Event() + bg_should_be_clear = threading.Event() + ok_to_exit_bg_thread = threading.Event() + + def f(): + g1 = RawGreenlet(fmain) + g1.switch(seen) + someref.append(g1) + del g1 + gc.collect() + bg_glet_created_running_and_no_longer_ref_in_bg.set() + fg_ref_released.wait(3) + + RawGreenlet() # trigger release + bg_should_be_clear.set() + ok_to_exit_bg_thread.wait(3) + RawGreenlet() # One more time + + t = threading.Thread(target=f) + t.start() + bg_glet_created_running_and_no_longer_ref_in_bg.wait(10) + + self.assertEqual(seen, []) + self.assertEqual(len(someref), 1) + del someref[:] + if not RUNNING_ON_FREETHREAD_BUILD: + # The free-threaded GC is very different. In 3.14rc1, + # the free-threaded GC traverses ``g1``, realizes it is + # not referenced from anywhere else IT cares about, + # calls ``tp_clear`` and then ``green_dealloc``. This causes + # the greenlet to lose its reference to the main greenlet and thread + # in which it was running, which means we can no longer throw an + # exception into it, preventing the rest of this test from working. + # Standard 3.14 traverses the object but doesn't ``tp_clear`` or + # ``green_dealloc`` it. + gc.collect() + # g1 is not released immediately because it's from another thread; + # switching back to that thread will allocate a greenlet and thus + # trigger deletion actions. + self.assertEqual(seen, []) + fg_ref_released.set() + bg_should_be_clear.wait(3) + try: + self.assertEqual(seen, [greenlet.GreenletExit]) + finally: + ok_to_exit_bg_thread.set() + t.join(10) + del seen[:] + del someref[:] + + def test_frame(self): + def f1(): + f = sys._getframe(0) # pylint:disable=protected-access + self.assertEqual(f.f_back, None) + greenlet.getcurrent().parent.switch(f) + return "meaning of life" + g = RawGreenlet(f1) + frame = g.switch() + self.assertTrue(frame is g.gr_frame) + self.assertTrue(g) + + from_g = g.switch() + self.assertFalse(g) + self.assertEqual(from_g, 'meaning of life') + self.assertEqual(g.gr_frame, None) + + def test_thread_bug(self): + def runner(x): + g = RawGreenlet(lambda: time.sleep(x)) + g.switch() + t1 = threading.Thread(target=runner, args=(0.2,)) + t2 = threading.Thread(target=runner, args=(0.3,)) + t1.start() + t2.start() + t1.join(10) + t2.join(10) + + def test_switch_kwargs(self): + def run(a, b): + self.assertEqual(a, 4) + self.assertEqual(b, 2) + return 42 + x = RawGreenlet(run).switch(a=4, b=2) + self.assertEqual(x, 42) + + def test_switch_kwargs_to_parent(self): + def run(x): + greenlet.getcurrent().parent.switch(x=x) + greenlet.getcurrent().parent.switch(2, x=3) + return x, x ** 2 + g = RawGreenlet(run) + self.assertEqual({'x': 3}, g.switch(3)) + self.assertEqual(((2,), {'x': 3}), g.switch()) + self.assertEqual((3, 9), g.switch()) + + def test_switch_to_another_thread(self): + data = {} + created_event = threading.Event() + done_event = threading.Event() + + def run(): + data['g'] = RawGreenlet(lambda: None) + created_event.set() + done_event.wait(10) + thread = threading.Thread(target=run) + thread.start() + created_event.wait(10) + with self.assertRaises(greenlet.error): + data['g'].switch() + done_event.set() + thread.join(10) + # XXX: Should handle this automatically + data.clear() + + def test_exc_state(self): + def f(): + try: + raise ValueError('fun') + except: # pylint:disable=bare-except + exc_info = sys.exc_info() + RawGreenlet(h).switch() + self.assertEqual(exc_info, sys.exc_info()) + + def h(): + self.assertEqual(sys.exc_info(), (None, None, None)) + + RawGreenlet(f).switch() + + def test_instance_dict(self): + def f(): + greenlet.getcurrent().test = 42 + def deldict(g): + del g.__dict__ + def setdict(g, value): + g.__dict__ = value + g = RawGreenlet(f) + self.assertEqual(g.__dict__, {}) + g.switch() + self.assertEqual(g.test, 42) + self.assertEqual(g.__dict__, {'test': 42}) + g.__dict__ = g.__dict__ + self.assertEqual(g.__dict__, {'test': 42}) + self.assertRaises(TypeError, deldict, g) + self.assertRaises(TypeError, setdict, g, 42) + + def test_running_greenlet_has_no_run(self): + has_run = [] + def func(): + has_run.append( + hasattr(greenlet.getcurrent(), 'run') + ) + + g = RawGreenlet(func) + g.switch() + self.assertEqual(has_run, [False]) + + def test_deepcopy(self): + import copy + self.assertRaises(TypeError, copy.copy, RawGreenlet()) + self.assertRaises(TypeError, copy.deepcopy, RawGreenlet()) + + def test_parent_restored_on_kill(self): + hub = RawGreenlet(lambda: None) + main = greenlet.getcurrent() + result = [] + def worker(): + try: + # Wait to be killed by going back to the test. + main.switch() + except greenlet.GreenletExit: + # Resurrect and switch to parent + result.append(greenlet.getcurrent().parent) + result.append(greenlet.getcurrent()) + hub.switch() + g = RawGreenlet(worker, parent=hub) + g.switch() + # delete the only reference, thereby raising GreenletExit + del g + self.assertTrue(result) + self.assertIs(result[0], main) + self.assertIs(result[1].parent, hub) + # Delete them, thereby breaking the cycle between the greenlet + # and the frame, which otherwise would never be collectable + # XXX: We should be able to automatically fix this. + del result[:] + hub = None + main = None + + def test_parent_return_failure(self): + # No run causes AttributeError on switch + g1 = RawGreenlet() + # Greenlet that implicitly switches to parent + g2 = RawGreenlet(lambda: None, parent=g1) + # AttributeError should propagate to us, no fatal errors + with self.assertRaises(AttributeError): + g2.switch() + + def test_throw_exception_not_lost(self): + class mygreenlet(RawGreenlet): + def __getattribute__(self, name): + try: + raise Exception # pylint:disable=broad-exception-raised + except: # pylint:disable=bare-except + pass + return RawGreenlet.__getattribute__(self, name) + g = mygreenlet(lambda: None) + self.assertRaises(SomeError, g.throw, SomeError()) + + @fails_leakcheck_on_py314_or_less + def _do_test_throw_to_dead_thread_doesnt_crash(self, wait_for_cleanup=False): + result = [] + def worker(): + greenlet.getcurrent().parent.switch() + + def creator(): + g = RawGreenlet(worker) + g.switch() + result.append(g) + if wait_for_cleanup: + # Let this greenlet eventually be cleaned up. + g.switch() + greenlet.getcurrent() + t = threading.Thread(target=creator) + t.start() + t.join(10) + del t + # But, depending on the operating system, the thread + # deallocator may not actually have run yet! So we can't be + # sure about the error message unless we wait. + if wait_for_cleanup: + self.wait_for_pending_cleanups() + with self.assertRaises(greenlet.error) as exc: + result[0].throw(SomeError) + + if not wait_for_cleanup: + s = str(exc.exception) + self.assertTrue( + s == "cannot switch to a different thread (which happens to have exited)" + or 'Cannot switch' in s + ) + else: + self.assertEqual( + str(exc.exception), + "cannot switch to a different thread (which happens to have exited)", + ) + + if hasattr(result[0].gr_frame, 'clear'): + # The frame is actually executing (it thinks), we can't clear it. + with self.assertRaises(RuntimeError): + result[0].gr_frame.clear() + # Unfortunately, this doesn't actually clear the references, they're in the + # fast local array. + if not wait_for_cleanup: + # f_locals has no clear method in Python 3.13 + if hasattr(result[0].gr_frame.f_locals, 'clear'): + result[0].gr_frame.f_locals.clear() + else: + self.assertIsNone(result[0].gr_frame) + + del creator + worker = None + del result[:] + # XXX: we ought to be able to automatically fix this. + # See issue 252 + self.expect_greenlet_leak = True # direct us not to wait for it to go away + + @fails_leakcheck_on_py314_or_less + @ignores_leakcheck_if( + WIN and sys.version_info[:2] == (3, 10), + "does not leaks on Windows 3.10, but does on other platforms" + ) + def test_throw_to_dead_thread_doesnt_crash(self): + self._do_test_throw_to_dead_thread_doesnt_crash() + + def test_throw_to_dead_thread_doesnt_crash_wait(self): + self._do_test_throw_to_dead_thread_doesnt_crash(True) + + @fails_leakcheck + def test_recursive_startup(self): + class convoluted(RawGreenlet): + def __init__(self): + RawGreenlet.__init__(self) + self.count = 0 + def __getattribute__(self, name): + if name == 'run' and self.count == 0: + self.count = 1 + self.switch(43) + return RawGreenlet.__getattribute__(self, name) + def run(self, value): + while True: + self.parent.switch(value) + g = convoluted() + self.assertEqual(g.switch(42), 43) + # Exits the running greenlet, otherwise it leaks + # XXX: We should be able to automatically fix this + #g.throw(greenlet.GreenletExit) + #del g + self.expect_greenlet_leak = True + + def test_threaded_updatecurrent(self): + # released when main thread should execute + lock1 = threading.Lock() + lock1.acquire() + # released when another thread should execute + lock2 = threading.Lock() + lock2.acquire() + class finalized(object): + def __del__(self): + # happens while in green_updatecurrent() in main greenlet + # should be very careful not to accidentally call it again + # at the same time we must make sure another thread executes + lock2.release() + lock1.acquire() + # now ts_current belongs to another thread + def deallocator(): + greenlet.getcurrent().parent.switch() + def fthread(): + lock2.acquire() + greenlet.getcurrent() + del g[0] + lock1.release() + lock2.acquire() + greenlet.getcurrent() + lock1.release() + main = greenlet.getcurrent() + g = [RawGreenlet(deallocator)] + g[0].bomb = finalized() + g[0].switch() + t = threading.Thread(target=fthread) + t.start() + # let another thread grab ts_current and deallocate g[0] + lock2.release() + lock1.acquire() + # this is the corner stone + # getcurrent() will notice that ts_current belongs to another thread + # and start the update process, which would notice that g[0] should + # be deallocated, and that will execute an object's finalizer. Now, + # that object will let another thread run so it can grab ts_current + # again, which would likely crash the interpreter if there's no + # check for this case at the end of green_updatecurrent(). This test + # passes if getcurrent() returns correct result, but it's likely + # to randomly crash if it's not anyway. + self.assertEqual(greenlet.getcurrent(), main) + # wait for another thread to complete, just in case + t.join(10) + + def test_dealloc_switch_args_not_lost(self): + seen = [] + def worker(): + # wait for the value + value = greenlet.getcurrent().parent.switch() + # delete all references to ourself + del worker[0] + initiator.parent = greenlet.getcurrent().parent + # switch to main with the value, but because + # ts_current is the last reference to us we + # return here immediately, where we resurrect ourself. + try: + greenlet.getcurrent().parent.switch(value) + finally: + seen.append(greenlet.getcurrent()) + def initiator(): + return 42 # implicitly falls thru to parent + + worker = [RawGreenlet(worker)] + + worker[0].switch() # prime worker + initiator = RawGreenlet(initiator, worker[0]) + value = initiator.switch() + self.assertTrue(seen) + self.assertEqual(value, 42) + + def test_tuple_subclass(self): + # The point of this test is to see what happens when a custom + # tuple subclass is used as an object passed directly to the C + # function ``green_switch``; part of ``green_switch`` checks + # the ``len()`` of the ``args`` tuple, and that can call back + # into Python. Here, when it calls back into Python, we + # recursively enter ``green_switch`` again. + + # This test is really only relevant on Python 2. The builtin + # `apply` function directly passes the given args tuple object + # to the underlying function, whereas the Python 3 version + # unpacks and repacks into an actual tuple. This could still + # happen using the C API on Python 3 though. We should write a + # builtin version of apply() ourself. + def _apply(func, a, k): + func(*a, **k) + + class mytuple(tuple): + def __len__(self): + greenlet.getcurrent().switch() + return tuple.__len__(self) + args = mytuple() + kwargs = dict(a=42) + def switchapply(): + _apply(greenlet.getcurrent().parent.switch, args, kwargs) + g = RawGreenlet(switchapply) + self.assertEqual(g.switch(), kwargs) + + def test_abstract_subclasses(self): + AbstractSubclass = ABCMeta( + 'AbstractSubclass', + (RawGreenlet,), + {'run': abstractmethod(lambda self: None)}) + + class BadSubclass(AbstractSubclass): + pass + + class GoodSubclass(AbstractSubclass): + def run(self): + pass + + GoodSubclass() # should not raise + self.assertRaises(TypeError, BadSubclass) + + def test_implicit_parent_with_threads(self): + if not gc.isenabled(): + return # cannot test with disabled gc + N = gc.get_threshold()[0] + if N < 50: + return # cannot test with such a small N + def attempt(): + lock1 = threading.Lock() + lock1.acquire() + lock2 = threading.Lock() + lock2.acquire() + recycled = [False] + def another_thread(): + lock1.acquire() # wait for gc + greenlet.getcurrent() # update ts_current + lock2.release() # release gc + t = threading.Thread(target=another_thread) + t.start() + class gc_callback(object): + def __del__(self): + lock1.release() + lock2.acquire() + recycled[0] = True + class garbage(object): + def __init__(self): + self.cycle = self + self.callback = gc_callback() + l = [] + x = range(N*2) + current = greenlet.getcurrent() + g = garbage() + for _ in x: + g = None # lose reference to garbage + if recycled[0]: + # gc callback called prematurely + t.join(10) + return False + last = RawGreenlet() + if recycled[0]: + break # yes! gc called in green_new + l.append(last) # increase allocation counter + else: + # gc callback not called when expected + gc.collect() + if recycled[0]: + t.join(10) + return False + self.assertEqual(last.parent, current) + for g in l: + self.assertEqual(g.parent, current) + return True + for _ in range(5): + if attempt(): + break + + def test_issue_245_reference_counting_subclass_no_threads(self): + # https://github.com/python-greenlet/greenlet/issues/245 + # Before the fix, this crashed pretty reliably on + # Python 3.10, at least on macOS; but much less reliably on other + # interpreters (memory layout must have changed). + # The threaded test crashed more reliably on more interpreters. + from greenlet import getcurrent + from greenlet import GreenletExit + + class Greenlet(RawGreenlet): + pass + + initial_refs = sys.getrefcount(Greenlet) + # This has to be an instance variable because + # Python 2 raises a SyntaxError if we delete a local + # variable referenced in an inner scope. + self.glets = [] # pylint:disable=attribute-defined-outside-init + + def greenlet_main(): + try: + getcurrent().parent.switch() + except GreenletExit: + self.glets.append(getcurrent()) + + # Before the + for _ in range(10): + Greenlet(greenlet_main).switch() + + del self.glets + if RUNNING_ON_FREETHREAD_BUILD: + # Free-threaded builds make types immortal, which gives us + # weird numbers here, and we actually do APPEAR to end + # up with one more reference than we started with, at least on 3.14. + # If we change the code in green_dealloc to avoid increffing the type + # (which fixed this initial bug), then our leakchecks find other objects + # that have leaked, including a tuple, a dict, and a type. So that's not the + # right solution. Instead we change the test: + # XXX: FIXME: Is there a better way? + self.assertGreaterEqual(sys.getrefcount(Greenlet), initial_refs) + else: + self.assertEqual(sys.getrefcount(Greenlet), initial_refs) + + @unittest.skipIf( + PY313 and RUNNING_ON_MANYLINUX, + "The manylinux images appear to hang on this test on 3.13rc2" + # Or perhaps I just got tired of waiting for the 450s timeout. + # Still, it shouldn't take anywhere near that long. Does not reproduce in + # Ubuntu images, on macOS or Windows. + ) + def test_issue_245_reference_counting_subclass_threads(self): + # https://github.com/python-greenlet/greenlet/issues/245 + from threading import Thread + from threading import Event + + from greenlet import getcurrent + + class MyGreenlet(RawGreenlet): + pass + + glets = [] + ref_cleared = Event() + + def greenlet_main(): + getcurrent().parent.switch() + + def thread_main(greenlet_running_event): + mine = MyGreenlet(greenlet_main) + glets.append(mine) + # The greenlets being deleted must be active + mine.switch() + # Don't keep any reference to it in this thread + del mine + # Let main know we published our greenlet. + greenlet_running_event.set() + # Wait for main to let us know the references are + # gone and the greenlet objects no longer reachable + ref_cleared.wait(10) + # The creating thread must call getcurrent() (or a few other + # greenlet APIs) because that's when the thread-local list of dead + # greenlets gets cleared. + getcurrent() + + # We start with 3 references to the subclass: + # - This module + # - Its __mro__ + # - The __subclassess__ attribute of greenlet + # - (If we call gc.get_referents(), we find four entries, including + # some other tuple ``(greenlet)`` that I'm not sure about but must be part + # of the machinery.) + # + # On Python 3.10 it's often enough to just run 3 threads; on Python 2.7, + # more threads are needed, and the results are still + # non-deterministic. Presumably the memory layouts are different + initial_refs = sys.getrefcount(MyGreenlet) + thread_ready_events = [] + thread_count = initial_refs + 45 + if RUNNING_ON_FREETHREAD_BUILD: + # types are immortal, so this is a HUGE number most likely, + # and we can't create that many threads. + thread_count = 50 + for _ in range(thread_count): + event = Event() + thread = Thread(target=thread_main, args=(event,)) + thread_ready_events.append(event) + thread.start() + + + for done_event in thread_ready_events: + done_event.wait(10) + + + del glets[:] + ref_cleared.set() + # Let any other thread run; it will crash the interpreter + # if not fixed (or silently corrupt memory and we possibly crash + # later). + self.wait_for_pending_cleanups() + self.assertEqual(sys.getrefcount(MyGreenlet), initial_refs) + + def test_falling_off_end_switches_to_unstarted_parent_raises_error(self): + def no_args(): + return 13 + + parent_never_started = RawGreenlet(no_args) + + def leaf(): + return 42 + + child = RawGreenlet(leaf, parent_never_started) + + # Because the run function takes to arguments + with self.assertRaises(TypeError): + child.switch() + + def test_falling_off_end_switches_to_unstarted_parent_works(self): + def one_arg(x): + return (x, 24) + + parent_never_started = RawGreenlet(one_arg) + + def leaf(): + return 42 + + child = RawGreenlet(leaf, parent_never_started) + + result = child.switch() + self.assertEqual(result, (42, 24)) + + def test_switch_to_dead_greenlet_with_unstarted_perverse_parent(self): + class Parent(RawGreenlet): + def __getattribute__(self, name): + if name == 'run': + raise SomeError + + + parent_never_started = Parent() + seen = [] + child = RawGreenlet(lambda: seen.append(42), parent_never_started) + # Because we automatically start the parent when the child is + # finished + with self.assertRaises(SomeError): + child.switch() + + self.assertEqual(seen, [42]) + + with self.assertRaises(SomeError): + child.switch() + self.assertEqual(seen, [42]) + + def test_switch_to_dead_greenlet_reparent(self): + seen = [] + parent_never_started = RawGreenlet(lambda: seen.append(24)) + child = RawGreenlet(lambda: seen.append(42)) + + child.switch() + self.assertEqual(seen, [42]) + + child.parent = parent_never_started + # This actually is the same as switching to the parent. + result = child.switch() + self.assertIsNone(result) + self.assertEqual(seen, [42, 24]) + + def test_can_access_f_back_of_suspended_greenlet(self): + # This tests our frame rewriting to work around Python 3.12+ having + # some interpreter frames on the C stack. It will crash in the absence + # of that logic. + main = greenlet.getcurrent() + + def outer(): + inner() + + def inner(): + main.switch(sys._getframe(0)) + + hub = RawGreenlet(outer) + # start it + hub.switch() + + # start another greenlet to make sure we aren't relying on + # anything in `hub` still being on the C stack + unrelated = RawGreenlet(lambda: None) + unrelated.switch() + + # now it is suspended + self.assertIsNotNone(hub.gr_frame) + self.assertEqual(hub.gr_frame.f_code.co_name, "inner") + self.assertIsNotNone(hub.gr_frame.f_back) + self.assertEqual(hub.gr_frame.f_back.f_code.co_name, "outer") + # The next line is what would crash + self.assertIsNone(hub.gr_frame.f_back.f_back) + + def test_get_stack_with_nested_c_calls(self): + from functools import partial + from . import _test_extension_cpp + + def recurse(v): + if v > 0: + return v * _test_extension_cpp.test_call(partial(recurse, v - 1)) + return greenlet.getcurrent().parent.switch() + + gr = RawGreenlet(recurse) + gr.switch(5) + frame = gr.gr_frame + for i in range(5): + self.assertEqual(frame.f_locals["v"], i) + frame = frame.f_back + self.assertEqual(frame.f_locals["v"], 5) + self.assertIsNone(frame.f_back) + self.assertEqual(gr.switch(10), 1200) # 1200 = 5! * 10 + + def test_frames_always_exposed(self): + # On Python 3.12 this will crash if we don't set the + # gr_frames_always_exposed attribute. More background: + # https://github.com/python-greenlet/greenlet/issues/388 + main = greenlet.getcurrent() + + def outer(): + inner(sys._getframe(0)) + + def inner(frame): + main.switch(frame) + + gr = RawGreenlet(outer) + frame = gr.switch() + + # Do something else to clobber the part of the C stack used by `gr`, + # so we can't skate by on "it just happened to still be there" + unrelated = RawGreenlet(lambda: None) + unrelated.switch() + + self.assertEqual(frame.f_code.co_name, "outer") + # The next line crashes on 3.12 if we haven't exposed the frames. + self.assertIsNone(frame.f_back) + + def test_switching_holding_critical_section_no_crash(self): + # https://github.com/python-greenlet/greenlet/issues/513 + # In no-GIL builds, we were failing to restore the + # critical_section pointer, leading to + # ``Fatal Python error: PyMutex_Unlock: unlocking mutex that is not locked`` + # when two greenlets both held a lock on an object, and then + # the GIL was released, which, according to + # ``Include/cpython/critical_section.h`` causes the critical + # section to get released. + # The field is present on Python 3.13+, and only used + # in no-GIL builds. + # Initial version of this test case provided in the + # github issue by ddorian + def k1(x): + g2.switch() # into G2 while holding G1's sort critical section + return x + + + def k2(x): + g1.switch() # back into G1 while holding G2's sort critical section + return x + + + def g1_body(): + sorted([0], key=k1) + g2.switch() + + + def g2_body(): + sorted([0], key=k2) + # Do a blocking I/O operation which would cause the + # critical sections to get suspended + with open(__file__, "rt", encoding='utf-8') as f: + f.read() + + + g1 = greenlet.greenlet(g1_body) + g2 = greenlet.greenlet(g2_body) + g1.switch() + + +class TestGreenletSetParentErrors(TestCase): + def test_threaded_reparent(self): + data = {} + created_event = threading.Event() + done_event = threading.Event() + + def run(): + data['g'] = RawGreenlet(lambda: None) + created_event.set() + done_event.wait(10) + + def blank(): + greenlet.getcurrent().parent.switch() + + thread = threading.Thread(target=run) + thread.start() + created_event.wait(10) + g = RawGreenlet(blank) + g.switch() + with self.assertRaises(ValueError) as exc: + g.parent = data['g'] + done_event.set() + thread.join(10) + + self.assertEqual(str(exc.exception), "parent cannot be on a different thread") + + def test_unexpected_reparenting(self): + another = [] + def worker(): + g = RawGreenlet(lambda: None) + another.append(g) + g.switch() + t = threading.Thread(target=worker) + t.start() + t.join(10) + # The first time we switch (running g_initialstub(), which is + # when we look up the run attribute) we attempt to change the + # parent to one from another thread (which also happens to be + # dead). ``g_initialstub()`` should detect this and raise a + # greenlet error. + # + # EXCEPT: With the fix for #252, this is actually detected + # sooner, when setting the parent itself. Prior to that fix, + # the main greenlet from the background thread kept a valid + # value for ``run_info``, and appeared to be a valid parent + # until we actually started the greenlet. But now that it's + # cleared, this test is catching whether ``green_setparent`` + # can detect the dead thread. + # + # Further refactoring once again changes this back to a greenlet.error + # + # We need to wait for the cleanup to happen, but we're + # deliberately leaking a main greenlet here. + self.wait_for_pending_cleanups(initial_main_greenlets=self.main_greenlets_before_test + 1) + + class convoluted(RawGreenlet): + def __getattribute__(self, name): + if name == 'run': + self.parent = another[0] # pylint:disable=attribute-defined-outside-init + return RawGreenlet.__getattribute__(self, name) + g = convoluted(lambda: None) + with self.assertRaises(greenlet.error) as exc: + g.switch() + self.assertEqual(str(exc.exception), + "cannot switch to a different thread (which happens to have exited)") + del another[:] + + def test_unexpected_reparenting_thread_running(self): + # Like ``test_unexpected_reparenting``, except the background thread is + # actually still alive. + another = [] + switched_to_greenlet = threading.Event() + keep_main_alive = threading.Event() + def worker(): + g = RawGreenlet(lambda: None) + another.append(g) + g.switch() + switched_to_greenlet.set() + keep_main_alive.wait(10) + class convoluted(RawGreenlet): + def __getattribute__(self, name): + if name == 'run': + self.parent = another[0] # pylint:disable=attribute-defined-outside-init + return RawGreenlet.__getattribute__(self, name) + + t = threading.Thread(target=worker) + t.start() + + switched_to_greenlet.wait(10) + try: + g = convoluted(lambda: None) + + with self.assertRaises(greenlet.error) as exc: + g.switch() + self.assertIn("Cannot switch to a different thread", str(exc.exception)) + self.assertIn("Expected", str(exc.exception)) + self.assertIn("Current", str(exc.exception)) + finally: + keep_main_alive.set() + t.join(10) + # XXX: Should handle this automatically. + del another[:] + + def test_cannot_delete_parent(self): + worker = RawGreenlet(lambda: None) + self.assertIs(worker.parent, greenlet.getcurrent()) + + with self.assertRaises(AttributeError) as exc: + del worker.parent + self.assertEqual(str(exc.exception), "can't delete attribute") + + def test_cannot_delete_parent_of_main(self): + with self.assertRaises(AttributeError) as exc: + del greenlet.getcurrent().parent + self.assertEqual(str(exc.exception), "can't delete attribute") + + + def test_main_greenlet_parent_is_none(self): + # assuming we're in a main greenlet here. + self.assertIsNone(greenlet.getcurrent().parent) + + def test_set_parent_wrong_types(self): + def bg(): + # Go back to main. + greenlet.getcurrent().parent.switch() + + def check(glet): + for p in None, 1, self, "42": + with self.assertRaises(TypeError) as exc: + glet.parent = p + + self.assertEqual( + str(exc.exception), + "GreenletChecker: Expected any type of greenlet, not " + type(p).__name__) + + # First, not running + g = RawGreenlet(bg) + self.assertFalse(g) + check(g) + + # Then when running. + g.switch() + self.assertTrue(g) + check(g) + + # Let it finish + g.switch() + + + def test_trivial_cycle(self): + glet = RawGreenlet(lambda: None) + with self.assertRaises(ValueError) as exc: + glet.parent = glet + self.assertEqual(str(exc.exception), "cyclic parent chain") + + def test_trivial_cycle_main(self): + # This used to produce a ValueError, but we catch it earlier than that now. + with self.assertRaises(AttributeError) as exc: + greenlet.getcurrent().parent = greenlet.getcurrent() + self.assertEqual(str(exc.exception), "cannot set the parent of a main greenlet") + + def test_deeper_cycle(self): + g1 = RawGreenlet(lambda: None) + g2 = RawGreenlet(lambda: None) + g3 = RawGreenlet(lambda: None) + + g1.parent = g2 + g2.parent = g3 + with self.assertRaises(ValueError) as exc: + g3.parent = g1 + self.assertEqual(str(exc.exception), "cyclic parent chain") + + +class TestRepr(TestCase): + + if not hasattr(TestCase, 'assertEndsWith'): # Added in 3.14 + def assertEndsWith(self, s, suffix, msg=None): + self.assertTrue(s.endswith(suffix), (s, suffix, msg)) + + def test_main_while_running(self): + r = repr(greenlet.getcurrent()) + self.assertEndsWith(r, " current active started main>") + + def test_main_in_background(self): + main = greenlet.getcurrent() + def run(): + return repr(main) + + g = RawGreenlet(run) + r = g.switch() + self.assertEndsWith(r, ' suspended active started main>') + + def test_initial(self): + r = repr(RawGreenlet()) + self.assertEndsWith(r, ' pending>') + + def test_main_from_other_thread(self): + main = greenlet.getcurrent() + + class T(threading.Thread): + original_main = thread_main = None + main_glet = None + def run(self): + self.original_main = repr(main) + self.main_glet = greenlet.getcurrent() + self.thread_main = repr(self.main_glet) + + t = T() + t.start() + t.join(10) + + self.assertEndsWith(t.original_main, ' suspended active started main>') + self.assertEndsWith(t.thread_main, ' current active started main>') + # give the machinery time to notice the death of the thread, + # and clean it up. Note that we don't use + # ``expect_greenlet_leak`` or wait_for_pending_cleanups, + # because at this point we know we have an extra greenlet + # still reachable. + for _ in range(3): + time.sleep(0.001) + + # In the past, main greenlets, even from dead threads, never + # really appear dead. We have fixed that, and we also report + # that the thread is dead in the repr. (Do this multiple times + # to make sure that we don't self-modify and forget our state + # in the C++ code). + for _ in range(3): + self.assertTrue(t.main_glet.dead) + r = repr(t.main_glet) + self.assertEndsWith(r, ' (thread exited) dead>') + + def test_dead(self): + g = RawGreenlet(lambda: None) + g.switch() + self.assertEndsWith(repr(g), ' dead>') + self.assertNotIn('suspended', repr(g)) + self.assertNotIn('started', repr(g)) + self.assertNotIn('active', repr(g)) + + def test_formatting_produces_native_str(self): + # https://github.com/python-greenlet/greenlet/issues/218 + # %s formatting on Python 2 was producing unicode, not str. + + g_dead = RawGreenlet(lambda: None) + g_not_started = RawGreenlet(lambda: None) + g_cur = greenlet.getcurrent() + + for g in g_dead, g_not_started, g_cur: + + self.assertIsInstance( + '%s' % (g,), + str + ) + self.assertIsInstance( + '%r' % (g,), + str, + ) + + +class TestMainGreenlet(TestCase): + # Tests some implementation details, and relies on some + # implementation details. + + def _check_current_is_main(self): + # implementation detail + assert 'main' in repr(greenlet.getcurrent()) + + t = type(greenlet.getcurrent()) + assert 'main' not in repr(t) + return t + + def test_main_greenlet_type_can_be_subclassed(self): + main_type = self._check_current_is_main() + subclass = type('subclass', (main_type,), {}) + self.assertIsNotNone(subclass) + + def test_main_greenlet_is_greenlet(self): + self._check_current_is_main() + self.assertIsInstance(greenlet.getcurrent(), RawGreenlet) + + def test_greenlet_run(self): + def do_it(): + with self.assertRaises(AttributeError): + getattr(g, 'run') + + g = greenlet.greenlet(do_it) + g.switch() + with self.assertRaises(AttributeError): + getattr(g, 'run') + + +class TestBrokenGreenlets(TestCase): + # Tests for things that used to, or still do, terminate the interpreter. + # This often means doing unsavory things. + + def test_failed_to_initialstub(self): + def func(): + raise AssertionError("Never get here") + + + g = greenlet._greenlet.UnswitchableGreenlet(func) + g.force_switch_error = True + + with self.assertRaisesRegex(SystemError, + "Failed to switch stacks into a greenlet for the first time."): + g.switch() + + def test_failed_to_switch_into_running(self): + runs = [] + def func(): + runs.append(1) + greenlet.getcurrent().parent.switch() + runs.append(2) + greenlet.getcurrent().parent.switch() + runs.append(3) # pragma: no cover + + g = greenlet._greenlet.UnswitchableGreenlet(func) + g.switch() + self.assertEqual(runs, [1]) + g.switch() + self.assertEqual(runs, [1, 2]) + g.force_switch_error = True + + with self.assertRaisesRegex(SystemError, + "Failed to switch stacks into a running greenlet."): + g.switch() + + # If we stopped here, we would fail the leakcheck, because we've left + # the ``inner_bootstrap()`` C frame and its descendents hanging around, + # which have a bunch of Python references. They'll never get cleaned up + # if we don't let the greenlet finish. + g.force_switch_error = False + g.switch() + self.assertEqual(runs, [1, 2, 3]) + + def test_failed_to_slp_switch_into_running(self): + ex = self.assertScriptRaises('fail_slp_switch.py') + + self.assertIn('fail_slp_switch is running', ex.output) + self.assertIn(ex.returncode, self.get_expected_returncodes_for_aborted_process()) + + def test_reentrant_switch_two_greenlets(self): + # Before we started capturing the arguments in g_switch_finish, this could crash. + output = self.run_script('fail_switch_two_greenlets.py') + self.assertIn('In g1_run', output) + self.assertIn('TRACE', output) + self.assertIn('LEAVE TRACE', output) + self.assertIn('Falling off end of main', output) + self.assertIn('Falling off end of g1_run', output) + self.assertIn('Falling off end of g2', output) + + def test_reentrant_switch_three_greenlets(self): + # On debug builds of greenlet, this used to crash with an assertion error; + # on non-debug versions, it ran fine (which it should not do!). + # Now it always crashes correctly with a TypeError + ex = self.assertScriptRaises('fail_switch_three_greenlets.py', exitcodes=(1,)) + + self.assertIn('TypeError', ex.output) + self.assertIn('positional arguments', ex.output) + + def test_reentrant_switch_three_greenlets2(self): + # This actually passed on debug and non-debug builds. It + # should probably have been triggering some debug assertions + # but it didn't. + # + # I think the fixes for the above test also kicked in here. + output = self.run_script('fail_switch_three_greenlets2.py') + self.assertIn( + "RESULTS: [('trace', 'switch'), " + "('trace', 'switch'), ('g2 arg', 'g2 from tracefunc'), " + "('trace', 'switch'), ('main g1', 'from g2_run'), ('trace', 'switch'), " + "('g1 arg', 'g1 from main'), ('trace', 'switch'), ('main g2', 'from g1_run'), " + "('trace', 'switch'), ('g1 from parent', 'g1 from main 2'), ('trace', 'switch'), " + "('main g1.2', 'g1 done'), ('trace', 'switch'), ('g2 from parent', ()), " + "('trace', 'switch'), ('main g2.2', 'g2 done')]", + output + ) + + def test_reentrant_switch_GreenletAlreadyStartedInPython(self): + output = self.run_script('fail_initialstub_already_started.py') + + self.assertIn( + "RESULTS: ['Begin C', 'Switch to b from B.__getattribute__ in C', " + "('Begin B', ()), '_B_run switching to main', ('main from c', 'From B'), " + "'B.__getattribute__ back from main in C', ('Begin A', (None,)), " + "('A dead?', True, 'B dead?', True, 'C dead?', False), " + "'C done', ('main from c.2', None)]", + output + ) + + def test_reentrant_switch_run_callable_has_del(self): + output = self.run_script('fail_clearing_run_switches.py') + self.assertIn( + "RESULTS [" + "('G.__getattribute__', 'run'), ('RunCallable', '__del__'), " + "('main: g.switch()', 'from RunCallable'), ('run_func', 'enter')" + "]", + output + ) + + def test_switch_leaves_no_critical_section_held(self): + # Free-threaded builds take per-object locks via + # Py_BEGIN_CRITICAL_SECTION, tracked in tstate->critical_section. A + # switch swaps C stacks, so leaving those locks held strands them on the + # fiber we left: asyncio's Task.__step holds one on the running task + # across the step, and touching that task from a child fiber then + # deadlocked re-taking it. Out of process because a regression is a + # hang, not a catchable error. + # (repro: freethread_switch_deadlock.py in the repo root) + if not RUNNING_ON_FREETHREAD_BUILD: + self.skipTest("Only free-threaded builds take critical sections") + output = self.run_script('fail_switch_critical_section.py') + self.assertIn('SWITCH CS OK', output) + +class TestModule(TestCase): + + @unittest.skipUnless(hasattr(sys, '_is_gil_enabled'), + "Needs 3.13 and above for sys._is_gil_enabled") + def test_no_gil_on_free_threaded(self): + if RUNNING_ON_FREETHREAD_BUILD: + self.assertFalse(sys._is_gil_enabled()) + else: + self.assertTrue(sys._is_gil_enabled()) + +if __name__ == '__main__': + unittest.main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py new file mode 100644 index 0000000..66a7ec6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +""" +Tests for greenlets interacting with the CPython trash can API. + +The CPython trash can API is not designed to be re-entered from a +single thread. But this can happen using greenlets, if something +during the object deallocation process switches greenlets, and this second +greenlet then causes the trash can to get entered again. Here, we do this +very explicitly, but in other cases (like gevent) it could be arbitrarily more +complicated: for example, a weakref callback might try to acquire a lock that's +already held by another greenlet; that would allow a greenlet switch to occur. + +See https://github.com/gevent/gevent/issues/1909 + +This test is fragile and relies on details of the CPython +implementation (like most of the rest of this package): + + - We enter the trashcan and deferred deallocation after + ``_PyTrash_UNWIND_LEVEL`` calls. This constant, defined in + CPython's object.c, is generally 50. That's basically how many objects are required to + get us into the deferred deallocation situation. + + - The test fails by hitting an ``assert()`` in object.c; if the + build didn't enable assert, then we don't catch this. + + - If the test fails in that way, the interpreter crashes. +""" + +import unittest + + +class TestTrashCanReEnter(unittest.TestCase): + + def test_it(self): + try: + # pylint:disable-next=no-name-in-module + from greenlet._greenlet import get_tstate_trash_delete_nesting + except ImportError: + import sys + # Python 3.13 has no "trash delete nesting" anymore (but + # "delete later") Our test as written won't be able to check + # the nesting depth anymore, but on debug builds it should + # still be able to trigger a crash if we get things wrong. + # However, at least on 3.14, the greenlet switch in the test + # below never finds an active value for + # ``tstate->delete_later``, meaning this test isn't testing + # what we want it to. + assert sys.version_info[:2] >= (3, 13) + def get_tstate_trash_delete_nesting(): + return 0 + + # Try several times to trigger it, because it isn't 100% + # reliable. + for i in range(30): + with self.subTest(i=i): + self.check_it(get_tstate_trash_delete_nesting) + + def check_it(self, get_tstate_trash_delete_nesting): # pylint:disable=too-many-statements + import greenlet + main = greenlet.getcurrent() + + assert get_tstate_trash_delete_nesting() == 0 + + # We expect to be in deferred deallocation after this many + # deallocations have occurred. TODO: I wish we had a better way to do + # this --- that was before get_tstate_trash_delete_nesting; perhaps + # we can use that API to do better? (Probably not, it is non-functional in + # 3.13+) + TRASH_UNWIND_LEVEL = 500 # 50 is the nominal default on 3.12 + # How many objects to put in a container; it's the container that + # queues objects for deferred deallocation. + OBJECTS_PER_CONTAINER = 1000 + + class Dealloc: # define the class here because we alter class variables each time we run. + """ + An object with a ``__del__`` method. When it starts getting deallocated + from a deferred trash can run, it switches greenlets, allocates more objects + which then also go in the trash can. If we don't save state appropriately, + nesting gets out of order and we can crash the interpreter. + """ + + #: Has our deallocation actually run and switched greenlets? + #: When it does, this will be set to the current greenlet. This should + #: be happening in the main greenlet, so we check that down below. + SPAWNED = False + + #: Has the background greenlet run? + BG_RAN = False + + BG_GLET = None + + #: How many of these things have ever been allocated. + CREATED = 0 + + #: How many of these things have ever been deallocated. + DESTROYED = 0 + + #: How many were destroyed not in the main greenlet. There should always + #: be some. + #: If the test is broken or things change in the trashcan implementation, + #: this may not be correct. + DESTROYED_BG = 0 + + def __init__(self, sequence_number): + """ + :param sequence_number: The ordinal of this object during + one particular creation run. This is used to detect (guess, really) + when we have entered the trash can's deferred deallocation. + """ + self.i = sequence_number + Dealloc.CREATED += 1 + + def __del__(self): + if self.i == TRASH_UNWIND_LEVEL and not self.SPAWNED: + Dealloc.SPAWNED = greenlet.getcurrent() + other = Dealloc.BG_GLET = greenlet.greenlet(background_greenlet) + x = other.switch() + assert x == 42 + # It's important that we don't switch back to the greenlet, + # we leave it hanging there in an incomplete state. But we don't let it + # get collected, either. If we complete it now, while we're still + # in the scope of the initial trash can, things work out and we + # don't see the problem. We need this greenlet to complete + # at some point in the future, after we've exited this trash can invocation. + del other + elif self.i == 40 and greenlet.getcurrent() is not main: + Dealloc.BG_RAN = True + try: + main.switch(42) + except greenlet.GreenletExit as ex: + # We expect this; all references to us go away + # while we're still running, and we need to finish deleting + # ourself. + Dealloc.BG_RAN = type(ex) + del ex + + # Record the fact that we're dead last of all. This ensures that + # we actually get returned too. + Dealloc.DESTROYED += 1 + if greenlet.getcurrent() is not main: + Dealloc.DESTROYED_BG += 1 + + + def background_greenlet(): + # We direct through a second function, instead of + # directly calling ``make_some()``, so that we have complete + # control over when these objects are destroyed: we need them + # to be destroyed in the context of the background greenlet + t = make_some() + del t # Triggere deletion. + + def make_some(): + t = () + i = OBJECTS_PER_CONTAINER + while i: + # Nest the tuples; it's the recursion that gets us + # into trash. + t = (Dealloc(i), t) + i -= 1 + return t + + + some = make_some() + self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER) + self.assertEqual(Dealloc.DESTROYED, 0) + + # If we're going to crash, it should be on the following line. + # We only crash if ``assert()`` is enabled, of course. + del some + + # For non-debug builds of CPython, we won't crash. The best we can do is check + # the nesting level explicitly. + self.assertEqual(0, get_tstate_trash_delete_nesting()) + + # Discard this, raising GreenletExit into where it is waiting. + Dealloc.BG_GLET = None + # The same nesting level maintains. + self.assertEqual(0, get_tstate_trash_delete_nesting()) + + # We definitely cleaned some up in the background + self.assertGreater(Dealloc.DESTROYED_BG, 0) + + # Make sure all the cleanups happened. + self.assertIs(Dealloc.SPAWNED, main) + self.assertTrue(Dealloc.BG_RAN) + self.assertEqual(Dealloc.BG_RAN, greenlet.GreenletExit) + self.assertEqual(Dealloc.CREATED, Dealloc.DESTROYED ) + self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER * 2) + + import gc + gc.collect() + + +if __name__ == '__main__': + unittest.main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_interpreter_shutdown.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_interpreter_shutdown.py new file mode 100644 index 0000000..6cc0d77 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_interpreter_shutdown.py @@ -0,0 +1,821 @@ +# -*- coding: utf-8 -*- +""" +Tests for greenlet behavior during interpreter shutdown (Py_FinalizeEx). + +These tests are organized into four groups: + + A. Core safety (smoke): no crashes with active greenlets at shutdown. + B. Cleanup semantics: GreenletExit / finally still works during + normal thread exit (the standard production path). + C. Atexit "still works" tests: getcurrent() / greenlet construction + during atexit handlers registered AFTER greenlet import (i.e. + BEFORE greenlet's cleanup handler in LIFO order) must return + valid objects — verifies the guards don't over-block. + D. TDD-certified regression tests: getcurrent() must return None + when called AFTER greenlet's cleanup (GC finalization phase + or late atexit phase). These tests fail on greenlet 3.3.2 + and pass with the fix across Python 3.10-3.14. +""" +import sys +import subprocess +import unittest +import textwrap + +from greenlet.tests import TestCase + + +class TestInterpreterShutdown(TestCase): # pylint:disable=too-many-public-methods + + def _run_shutdown_script(self, script_body): + """ + Run a Python script in a subprocess that exercises greenlet + during interpreter shutdown. Returns (returncode, stdout, stderr). + """ + full_script = textwrap.dedent(script_body) + result = subprocess.run( + [sys.executable, '-c', full_script], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + return result.returncode, result.stdout, result.stderr + + # ----------------------------------------------------------------- + # Group A: Core safety — no crashes with active greenlets at exit + # ----------------------------------------------------------------- + + def test_active_greenlet_at_shutdown_no_crash(self): + + # An active (suspended) greenlet that is deallocated during + # interpreter shutdown should not crash the process. + + # Before the fix, this would SIGSEGV on Python < 3.11 because + # _green_dealloc_kill_started_non_main_greenlet tried to call + # g_switch() during Py_FinalizeEx. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def worker(): + greenlet.getcurrent().parent.switch("from worker") + return "done" + + g = greenlet.greenlet(worker) + result = g.switch() + assert result == "from worker", result + print("OK: exiting with active greenlet") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: exiting with active greenlet", stdout) + + def test_multiple_active_greenlets_at_shutdown(self): + # Multiple suspended greenlets at shutdown should all be cleaned + # up without crashing. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def worker(name): + greenlet.getcurrent().parent.switch(f"hello from {name}") + return "done" + + greenlets = [] + for i in range(10): + g = greenlet.greenlet(worker) + result = g.switch(f"g{i}") + greenlets.append(g) + + print(f"OK: {len(greenlets)} active greenlets at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets at shutdown", stdout) + + def test_nested_greenlets_at_shutdown(self): + # Nested (chained parent) greenlets at shutdown should not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def inner(): + greenlet.getcurrent().parent.switch("inner done") + + def outer(): + g_inner = greenlet.greenlet(inner) + g_inner.switch() + greenlet.getcurrent().parent.switch("outer done") + + g = greenlet.greenlet(outer) + result = g.switch() + assert result == "outer done", result + print("OK: nested greenlets at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: nested greenlets at shutdown", stdout) + + def test_threaded_greenlets_at_shutdown(self): + # Greenlets in worker threads that are still referenced at + # shutdown should not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + import threading + + results = [] + + def thread_worker(): + def greenlet_func(): + greenlet.getcurrent().parent.switch("from thread greenlet") + return "done" + + g = greenlet.greenlet(greenlet_func) + val = g.switch() + results.append((g, val)) + + threads = [] + for _ in range(3): + t = threading.Thread(target=thread_worker) + t.start() + threads.append(t) + + for t in threads: + t.join() + + print(f"OK: {len(results)} threaded greenlets at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 3 threaded greenlets at shutdown", stdout) + + # ----------------------------------------------------------------- + # Group B: Cleanup semantics — thread exit + # ----------------------------------------------------------------- + # + # These tests verify that GreenletExit / try-finally still work + # correctly during normal thread exit (the standard production + # path, e.g. uWSGI worker threads finishing a request). This is + # NOT interpreter shutdown; the guards do not fire here. + + def test_greenlet_cleanup_during_thread_exit(self): + # When a thread exits normally while holding active greenlets, + # GreenletExit IS thrown and cleanup code runs. This is the + # standard cleanup path used in production (e.g. uWSGI worker + # threads finishing a request). + + rc, stdout, stderr = self._run_shutdown_script("""\ + import os + import threading + import greenlet + + _write = os.write + + def thread_func(): + def worker(_w=_write, + _GreenletExit=greenlet.GreenletExit): + try: + greenlet.getcurrent().parent.switch("suspended") + except _GreenletExit: + _w(1, b"CLEANUP: GreenletExit caught\\n") + raise + + g = greenlet.greenlet(worker) + g.switch() + # Thread exits with active greenlet -> thread-state + # cleanup triggers GreenletExit + + t = threading.Thread(target=thread_func) + t.start() + t.join() + print("OK: thread cleanup done") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: thread cleanup done", stdout) + self.assertIn("CLEANUP: GreenletExit caught", stdout) + + def test_finally_block_during_thread_exit(self): + # try/finally blocks in active greenlets run correctly when the + # owning thread exits. + rc, stdout, stderr = self._run_shutdown_script("""\ + import os + import threading + import greenlet + + _write = os.write + + def thread_func(): + def worker(_w=_write): + try: + greenlet.getcurrent().parent.switch("suspended") + finally: + _w(1, b"FINALLY: cleanup executed\\n") + + g = greenlet.greenlet(worker) + g.switch() + + t = threading.Thread(target=thread_func) + t.start() + t.join() + print("OK: thread cleanup done") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: thread cleanup done", stdout) + self.assertIn("FINALLY: cleanup executed", stdout) + + def test_many_greenlets_with_cleanup_at_shutdown(self): + # Stress test: many active greenlets with cleanup code at shutdown. + # Ensures no crashes regardless of deallocation order. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import sys + import greenlet + + cleanup_count = 0 + + def worker(idx): + global cleanup_count + try: + greenlet.getcurrent().parent.switch(f"ready-{idx}") + except greenlet.GreenletExit: + cleanup_count += 1 + raise + + greenlets = [] + for i in range(50): + g = greenlet.greenlet(worker) + result = g.switch(i) + greenlets.append(g) + + print(f"OK: {len(greenlets)} greenlets about to shut down") + # Note: we can't easily print cleanup_count during shutdown + # since it happens after the main module's code runs. + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 50 greenlets about to shut down", stdout) + + def test_deeply_nested_greenlets_at_shutdown(self): + # Deeply nested greenlet parent chains at shutdown. + # Tests that the deallocation order doesn't cause issues. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def level(depth, max_depth): + if depth < max_depth: + g = greenlet.greenlet(level) + g.switch(depth + 1, max_depth) + greenlet.getcurrent().parent.switch(f"depth-{depth}") + + g = greenlet.greenlet(level) + result = g.switch(0, 10) + print(f"OK: nested to depth 10, got {result}") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: nested to depth 10", stdout) + + def test_greenlet_with_traceback_at_shutdown(self): + # A greenlet that has an active exception context when it's + # suspended should not crash during shutdown cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def worker(): + try: + raise ValueError("test error") + except ValueError: + # Suspend while an exception is active on the stack + greenlet.getcurrent().parent.switch("suspended with exc") + return "done" + + g = greenlet.greenlet(worker) + result = g.switch() + assert result == "suspended with exc" + print("OK: greenlet with active exception at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: greenlet with active exception at shutdown", stdout) + + # ----------------------------------------------------------------- + # Group C: getcurrent() / construction / gettrace() / settrace() + # during atexit — registered AFTER greenlet import + # ----------------------------------------------------------------- + + def test_getcurrent_during_atexit_no_crash(self): + # getcurrent() in an atexit handler registered AFTER greenlet + # import must return a valid greenlet (not None), because LIFO + # ordering means this handler runs BEFORE greenlet's cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def call_getcurrent_at_exit(): + try: + g = greenlet.getcurrent() + if g is not None and type(g).__name__ == 'greenlet': + print(f"OK: getcurrent returned valid greenlet") + elif g is None: + print("FAIL: getcurrent returned None (over-blocked)") + else: + print(f"FAIL: unexpected {g!r}") + except Exception as e: + print(f"OK: getcurrent raised {type(e).__name__}: {e}") + + atexit.register(call_getcurrent_at_exit) + print("OK: atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: atexit registered", stdout) + self.assertIn("OK: getcurrent returned valid greenlet", stdout, + "getcurrent() should return a valid greenlet when called " + "before greenlet's cleanup handler (LIFO ordering)") + + def test_gettrace_during_atexit_no_crash(self): + # Calling greenlet.gettrace() during atexit must not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def check_at_exit(): + try: + result = greenlet.gettrace() + print(f"OK: gettrace returned {result!r}") + except Exception as e: + print(f"OK: gettrace raised {type(e).__name__}: {e}") + + atexit.register(check_at_exit) + print("OK: registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: registered", stdout) + + def test_settrace_during_atexit_no_crash(self): + # Calling greenlet.settrace() during atexit must not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def check_at_exit(): + try: + greenlet.settrace(lambda *args: None) + print("OK: settrace succeeded") + except Exception as e: + print(f"OK: settrace raised {type(e).__name__}: {e}") + + atexit.register(check_at_exit) + print("OK: registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: registered", stdout) + + def test_getcurrent_with_active_greenlets_during_atexit(self): + # getcurrent() during atexit (registered after import) with active + # greenlets must still return a valid greenlet, since LIFO means + # this runs before greenlet's cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def worker(): + greenlet.getcurrent().parent.switch("ready") + + greenlets = [] + for i in range(5): + g = greenlet.greenlet(worker) + result = g.switch() + greenlets.append(g) + + def check_at_exit(): + try: + g = greenlet.getcurrent() + if g is not None and type(g).__name__ == 'greenlet': + print(f"OK: getcurrent returned valid greenlet") + elif g is None: + print("FAIL: getcurrent returned None (over-blocked)") + else: + print(f"FAIL: unexpected {g!r}") + except Exception as e: + print(f"OK: getcurrent raised {type(e).__name__}: {e}") + + atexit.register(check_at_exit) + print(f"OK: {len(greenlets)} active greenlets, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 5 active greenlets, atexit registered", stdout) + self.assertIn("OK: getcurrent returned valid greenlet", stdout, + "getcurrent() should return a valid greenlet when called " + "before greenlet's cleanup handler (LIFO ordering)") + + def test_greenlet_construction_during_atexit_no_crash(self): + # Constructing a new greenlet during atexit (registered after + # import) must succeed, since this runs before greenlet's cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def create_greenlets_at_exit(): + try: + def noop(): + pass + g = greenlet.greenlet(noop) + if g is not None: + print(f"OK: created greenlet successfully") + else: + print("FAIL: greenlet() returned None") + except Exception as e: + print(f"OK: construction raised {type(e).__name__}: {e}") + + atexit.register(create_greenlets_at_exit) + print("OK: atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: atexit registered", stdout) + self.assertIn("OK: created greenlet successfully", stdout) + + def test_greenlet_construction_with_active_greenlets_during_atexit(self): + # Constructing new greenlets during atexit when other active + # greenlets already exist (maximizes the chance of a non-empty + # deleteme list). + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def worker(): + greenlet.getcurrent().parent.switch("ready") + + greenlets = [] + for i in range(10): + g = greenlet.greenlet(worker) + g.switch() + greenlets.append(g) + + def create_at_exit(): + try: + new_greenlets = [] + for i in range(5): + g = greenlet.greenlet(lambda: None) + new_greenlets.append(g) + print(f"OK: created {len(new_greenlets)} greenlets at exit") + except Exception as e: + print(f"OK: raised {type(e).__name__}: {e}") + + atexit.register(create_at_exit) + print(f"OK: {len(greenlets)} active greenlets, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets, atexit registered", stdout) + + def test_greenlet_construction_with_cross_thread_deleteme_during_atexit(self): + # Create greenlets in a worker thread, transfer them to the main + # thread, then drop them — populating the deleteme list. Then + # construct a new greenlet during atexit. On Python < 3.11 + # clear_deleteme_list() could previously crash if the + # PythonAllocator vector copy failed during early Py_FinalizeEx; + # using std::swap eliminates that allocation. + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + import threading + + cross_thread_refs = [] + + def thread_worker(): + # Create greenlets in this thread + def gl_body(): + greenlet.getcurrent().parent.switch("ready") + for _ in range(20): + g = greenlet.greenlet(gl_body) + g.switch() + cross_thread_refs.append(g) + + t = threading.Thread(target=thread_worker) + t.start() + t.join() + + # Dropping these references in the main thread + # causes them to be added to the main thread's + # deleteme list (deferred cross-thread dealloc). + cross_thread_refs.clear() + + def create_at_exit(): + try: + g = greenlet.greenlet(lambda: None) + print(f"OK: created greenlet at exit {g!r}") + except Exception as e: + print(f"OK: raised {type(e).__name__}: {e}") + + atexit.register(create_at_exit) + print("OK: cross-thread setup done, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: cross-thread setup done, atexit registered", stdout) + + + # ----------------------------------------------------------------- + # Group D.1: TDD-certified — getcurrent() during GC finalization + # + # These tests use gc.disable() + reference cycles to force __del__ + # to run during Py_FinalizeEx's GC pass, where Py_IsFinalizing() + # is True. Without the fix, getcurrent() returns a live greenlet + # (unguarded); with the fix, it returns None. + # + # TDD verification (greenlet 3.3.2 = RED, patched = GREEN): + # Python 3.10: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.11: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.12: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.13: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.14: RED (UNGUARDED) → GREEN (GUARDED) + # ----------------------------------------------------------------- + + def test_getcurrent_returns_none_during_gc_finalization(self): + # greenlet.getcurrent() must raise an exception when called from a + # __del__ method during Py_FinalizeEx's GC collection pass. + + # On Python >= 3.11, _Py_IsFinalizing() is True during this + # phase. Without the Py_IsFinalizing() guard in mod_getcurrent, + # this would return a greenlet — the same unguarded code path + # that leads to SIGSEGV in production (uWSGI worker recycling). + rc, stdout, stderr = self._run_shutdown_script("""\ + import gc + import os + import greenlet + + gc.disable() + + class CleanupChecker: + def __del__(self): + try: + try: + greenlet.getcurrent() + except RuntimeError: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Reference cycle: only collected during Py_FinalizeEx GC pass + a = CleanupChecker() + b = {"ref": a} + a._cycle = b + del a, b + + print("OK: deferred cycle created") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: deferred cycle created", stdout) + self.assertIn("GUARDED: getcurrent=None", stdout) + + def test_getcurrent_returns_none_during_gc_finalization_with_active_greenlets(self): + # Same as above but with active greenlets at shutdown, which + # increases the amount of C++ destructor work during finalization. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import gc + import os + import greenlet + + gc.disable() + + class CleanupChecker: + def __del__(self): + try: + try: + greenlet.getcurrent() + except RuntimeError: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Create active greenlets + greenlets = [] + for _ in range(10): + def worker(): + greenlet.getcurrent().parent.switch("suspended") + g = greenlet.greenlet(worker) + g.switch() + greenlets.append(g) + + # Reference cycle deferred to Py_FinalizeEx + a = CleanupChecker() + b = {"ref": a} + a._cycle = b + del a, b + + print(f"OK: {len(greenlets)} active greenlets, cycle deferred") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets, cycle deferred", stdout) + self.assertIn("GUARDED: getcurrent=None", stdout) + + def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self): + # Combines cross-thread greenlet deallocation (deleteme list) + # with the GC finalization check. This simulates the production + # scenario where uWSGI worker threads create greenlets that are + # transferred to the main thread, then cleaned up during + # Py_FinalizeEx. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import gc + import os + import threading + import greenlet + + gc.disable() + + class CleanupChecker: + def __del__(self): + try: + try: + greenlet.getcurrent() + except RuntimeError: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Create cross-thread greenlet references + cross_refs = [] + def thread_fn(): + for _ in range(20): + def body(): + greenlet.getcurrent().parent.switch("x") + g = greenlet.greenlet(body) + g.switch() + cross_refs.append(g) + t = threading.Thread(target=thread_fn) + t.start() + t.join() + cross_refs.clear() + + # Reference cycle deferred to Py_FinalizeEx + a = CleanupChecker() + b = {"ref": a} + a._cycle = b + del a, b + + print("OK: cross-thread cleanup + cycle deferred") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: cross-thread cleanup + cycle deferred", stdout) + self.assertIn("GUARDED: getcurrent=None", stdout) + + + # ----------------------------------------------------------------- + # Group D.2: TDD-certified — getcurrent() during atexit phase + # + # These tests register the checker BEFORE importing greenlet. + # Python's atexit is LIFO, so greenlet's handler (registered at + # import) runs FIRST and sets g_greenlet_shutting_down=1; then + # the checker runs SECOND and observes getcurrent() → None. + # + # This covers the atexit phase where _Py_IsFinalizing() is still + # False on ALL Python versions — the exact window that causes + # SIGSEGV in production (uWSGI worker recycling → Py_FinalizeEx). + # + # TDD verification (greenlet 3.3.2 = RED, patched = GREEN): + # Python 3.10: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.11: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.12: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.13: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.14: RED (UNGUARDED) → GREEN (GUARDED) + # ----------------------------------------------------------------- + + def test_getcurrent_returns_none_during_atexit_phase(self): + # greenlet.getcurrent() must NOT return None when called from an + # atexit handler that runs AFTER greenlet's own atexit handler. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import os + + def late_checker(): + try: + import greenlet + cur = greenlet.getcurrent() + if cur is None: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Register BEFORE importing greenlet. LIFO order: + # greenlet's handler (registered at import) runs FIRST, + # late_checker runs SECOND — seeing the flag already set. + atexit.register(late_checker) + + import greenlet + print("OK: atexit registered before greenlet import") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: atexit registered before greenlet import", stdout) + self.assertIn("UNGUARDED", stdout) + + + def test_getcurrent_returns_none_during_atexit_phase_with_active_greenlets(self): + # Same as above but with active greenlets + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import os + + def late_checker(): + try: + import greenlet + cur = greenlet.getcurrent() + if cur is None: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + atexit.register(late_checker) + + import greenlet + + greenlets = [] + for _ in range(10): + def worker(): + greenlet.getcurrent().parent.switch("parked") + g = greenlet.greenlet(worker) + g.switch() + greenlets.append(g) + + print(f"OK: {len(greenlets)} active greenlets, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets, atexit registered", stdout) + self.assertIn("UNGUARDED", stdout) + + def test_api_getcurrent_no_system_error_at_module_gc_time(self): + # If we use the C API directly to return a greenlet AFTER + # atexit threads have been run, we don't crash, we get a + # specific error. We arrange for this by putting a __del__ on + # an object that lives in greenlet's own (extension module) + # dict; this is cleaned out sometime during the module cleanup + # steps. + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + from greenlet.tests import _test_extension + + class WithDel: + # must cache the method we want, because by the time we + # run, module globals may have been cleaned up. + def __del__(self, gc=_test_extension.getcurrent_api): + print('Destructor running') + gc() # Should print an unraisable RuntimeException + + greenlet._greenlet.with_del = WithDel() + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn('Destructor running', stdout) + self.assertIn('RuntimeError: greenlet is being finalized', stderr) + + + def test_switch_no_error_at_module_gc_time(self): + # Switching to a greenlet we've captured during + # module tear down doesn't cause a crash + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + from greenlet.tests import _test_extension + + gs = [] + # must cache the objects we want, because by the time we + # run, module globals may have been cleaned up. + def do_it(gs=gs): + print('current', gs) + gs[0].parent.switch(1) + + + gs.append(greenlet.greenlet(do_it)) + gs.append(greenlet.greenlet(do_it)) + gs[1].switch() + + class WithDel: + def __del__(self, gs=gs): + print('Destructor running') + r = gs[0].switch() + print('Result', r) + + greenlet._greenlet.with_del = WithDel() + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn('Destructor running', stdout) + self.assertIn('Result 1', stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_leaks.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_leaks.py new file mode 100644 index 0000000..12b9124 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_leaks.py @@ -0,0 +1,497 @@ +# -*- coding: utf-8 -*- +""" +Testing scenarios that may have leaked. +""" +import sys +import gc + +import time +import weakref +import threading + + +import greenlet +from . import TestCase +from . import PY314 +from . import RUNNING_ON_FREETHREAD_BUILD +from . import WIN +from .leakcheck import fails_leakcheck +from .leakcheck import ignores_leakcheck +from .leakcheck import RUNNING_ON_MANYLINUX + + +# pylint:disable=protected-access + +assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0 + +class HasFinalizerTracksInstances(object): + EXTANT_INSTANCES = set() + def __init__(self, msg): + self.msg = sys.intern(msg) + self.EXTANT_INSTANCES.add(id(self)) + def __del__(self): + self.EXTANT_INSTANCES.remove(id(self)) + def __repr__(self): + return "" % ( + id(self), self.msg + ) + @classmethod + def reset(cls): + cls.EXTANT_INSTANCES.clear() + + +def fails_leakcheck_except_on_free_thraded(func): + if RUNNING_ON_FREETHREAD_BUILD: + # These all seem to pass on free threading because + # of the changes to the garbage collector + return func + return fails_leakcheck(func) + + +class TestLeaks(TestCase): + + def test_arg_refs(self): + args = ('a', 'b', 'c') + refcount_before = sys.getrefcount(args) + # pylint:disable=unnecessary-lambda + g = greenlet.greenlet( + lambda *args: greenlet.getcurrent().parent.switch(*args)) + for _ in range(100): + g.switch(*args) + self.assertEqual(sys.getrefcount(args), refcount_before) + + def test_kwarg_refs(self): + kwargs = {} + self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1) + # pylint:disable=unnecessary-lambda + g = greenlet.greenlet( + lambda **gkwargs: greenlet.getcurrent().parent.switch(**gkwargs)) + for _ in range(100): + g.switch(**kwargs) + # Python 3.14 elides reference counting operations + # in some cases. See https://github.com/python/cpython/pull/130708 + self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1) + + + @staticmethod + def __recycle_threads(): + # By introducing a thread that does sleep we allow other threads, + # that have triggered their __block condition, but did not have a + # chance to deallocate their thread state yet, to finally do so. + # The way it works is by requiring a GIL switch (different thread), + # which does a GIL release (sleep), which might do a GIL switch + # to finished threads and allow them to clean up. + def worker(): + time.sleep(0.001) + t = threading.Thread(target=worker) + t.start() + time.sleep(0.001) + t.join(10) + + def test_threaded_leak(self): + gg = [] + def worker(): + # only main greenlet present + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): + t = threading.Thread(target=worker) + t.start() + t.join(10) + del t + greenlet.getcurrent() # update ts_current + self.__recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def test_threaded_adv_leak(self): + gg = [] + def worker(): + # main and additional *finished* greenlets + ll = greenlet.getcurrent().ll = [] + def additional(): + ll.append(greenlet.getcurrent()) + for _ in range(2): + greenlet.greenlet(additional).switch() + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): + t = threading.Thread(target=worker) + t.start() + t.join(10) + del t + greenlet.getcurrent() # update ts_current + self.__recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def assertClocksUsed(self): + used = greenlet._greenlet.get_clocks_used_doing_optional_cleanup() + self.assertGreaterEqual(used, 0) + # we don't lose the value + greenlet._greenlet.enable_optional_cleanup(True) + used2 = greenlet._greenlet.get_clocks_used_doing_optional_cleanup() + self.assertEqual(used, used2) + self.assertGreater(greenlet._greenlet.CLOCKS_PER_SEC, 1) + + def _check_issue251(self, + manually_collect_background=True, + explicit_reference_to_switch=False): + # See https://github.com/python-greenlet/greenlet/issues/251 + # Killing a greenlet (probably not the main one) + # in one thread from another thread would + # result in leaking a list (the ts_delkey list). + # We no longer use lists to hold that stuff, though. + + # For the test to be valid, even empty lists have to be tracked by the + # GC + + assert gc.is_tracked([]) + HasFinalizerTracksInstances.reset() + greenlet.getcurrent() + greenlets_before = self.count_objects(greenlet.greenlet, exact_kind=False) + + background_glet_running = threading.Event() + background_glet_killed = threading.Event() + background_greenlets = [] + + # XXX: Switching this to a greenlet subclass that overrides + # run results in all callers failing the leaktest; that + # greenlet instance is leaked. There's a bound method for + # run() living on the stack of the greenlet in g_initialstub, + # and since we don't manually switch back to the background + # greenlet to let it "fall off the end" and exit the + # g_initialstub function, it never gets cleaned up. Making the + # garbage collector aware of this bound method (making it an + # attribute of the greenlet structure and traversing into it) + # doesn't help, for some reason. + def background_greenlet(): + # Throw control back to the main greenlet. + jd = HasFinalizerTracksInstances("DELETING STACK OBJECT") + greenlet._greenlet.set_thread_local( + 'test_leaks_key', + HasFinalizerTracksInstances("DELETING THREAD STATE")) + # Explicitly keeping 'switch' in a local variable + # breaks this test in all versions + if explicit_reference_to_switch: + s = greenlet.getcurrent().parent.switch + s([jd]) + else: + greenlet.getcurrent().parent.switch([jd]) + + bg_main_wrefs = [] + + def background_thread(): + glet = greenlet.greenlet(background_greenlet) + bg_main_wrefs.append(weakref.ref(glet.parent)) + + background_greenlets.append(glet) + glet.switch() # Be sure it's active. + # Control is ours again. + del glet # Delete one reference from the thread it runs in. + background_glet_running.set() + background_glet_killed.wait(10) + + # To trigger the background collection of the dead + # greenlet, thus clearing out the contents of the list, we + # need to run some APIs. See issue 252. + if manually_collect_background: + greenlet.getcurrent() + + + t = threading.Thread(target=background_thread) + t.start() + background_glet_running.wait(10) + greenlet.getcurrent() + lists_before = self.count_objects(list, exact_kind=True) + + assert len(background_greenlets) == 1 + self.assertFalse(background_greenlets[0].dead) + # Delete the last reference to the background greenlet + # from a different thread. This puts it in the background thread's + # ts_delkey list. + del background_greenlets[:] + background_glet_killed.set() + + # Now wait for the background thread to die. + t.join(10) + del t + # As part of the fix for 252, we need to cycle the ceval.c + # interpreter loop to be sure it has had a chance to process + # the pending call. + self.wait_for_pending_cleanups() + + lists_after = self.count_objects(list, exact_kind=True) + greenlets_after = self.count_objects(greenlet.greenlet, exact_kind=False) + + # On 2.7, we observe that lists_after is smaller than + # lists_before. No idea what lists got cleaned up. All the + # Python 3 versions match exactly. + self.assertLessEqual(lists_after, lists_before) + # On versions after 3.6, we've successfully cleaned up the + # greenlet references thanks to the internal "vectorcall" + # protocol; prior to that, there is a reference path through + # the ``greenlet.switch`` method still on the stack that we + # can't reach to clean up. The C code goes through terrific + # lengths to clean that up. + if not explicit_reference_to_switch \ + and greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None: + # If cleanup was disabled, though, we may not find it. + self.assertEqual(greenlets_after, greenlets_before) + if manually_collect_background: + # TODO: Figure out how to make this work! + # The one on the stack is still leaking somehow + # in the non-manually-collect state. + self.assertEqual(HasFinalizerTracksInstances.EXTANT_INSTANCES, set()) + else: + # The explicit reference prevents us from collecting it + # and it isn't always found by the GC either for some + # reason. The entire frame is leaked somehow, on some + # platforms (e.g., MacPorts builds of Python (all + # versions!)), but not on other platforms (the linux and + # windows builds on GitHub actions and Appveyor). So we'd + # like to write a test that proves that the main greenlet + # sticks around, and we can on my machine (macOS 11.6, + # MacPorts builds of everything) but we can't write that + # same test on other platforms. However, hopefully iteration + # done by leakcheck will find it. + pass + + if greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None: + self.assertClocksUsed() + + def test_issue251_killing_cross_thread_leaks_list(self): + self._check_issue251() + + def test_issue251_with_cleanup_disabled(self): + greenlet._greenlet.enable_optional_cleanup(False) + try: + self._check_issue251() + finally: + greenlet._greenlet.enable_optional_cleanup(True) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_need_to_collect_in_background(self): + # Between greenlet 1.1.2 and the next version, this was still + # failing because the leak of the list still exists when we + # don't call a greenlet API before exiting the thread. The + # proximate cause is that neither of the two greenlets from + # the background thread are actually being destroyed, even + # though the GC is in fact visiting both objects. It's not + # clear where that leak is? For some reason the thread-local + # dict holding it isn't being cleaned up. + # + # The leak, I think, is in the CPYthon internal function that + # calls into green_switch(). The argument tuple is still on + # the C stack somewhere and can't be reached? That doesn't + # make sense, because the tuple should be collectable when + # this object goes away. + # + # Note that this test sometimes spuriously passes on Linux, + # for some reason, but I've never seen it pass on macOS. + self._check_issue251(manually_collect_background=False) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_need_to_collect_in_background_cleanup_disabled(self): + self.expect_greenlet_leak = True + greenlet._greenlet.enable_optional_cleanup(False) + try: + self._check_issue251(manually_collect_background=False) + finally: + greenlet._greenlet.enable_optional_cleanup(True) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_explicit_reference_not_collectable(self): + self._check_issue251( + manually_collect_background=False, + explicit_reference_to_switch=True) + + UNTRACK_ATTEMPTS = 100 + + def _only_test_some_versions(self): + # We're only looking for this problem specifically on 3.11, + # and this set of tests is relatively fragile, depending on + # OS and memory management details. So we want to run it on 3.11+ + # (obviously) but not every older 3.x version in order to reduce + # false negatives. At the moment, those false results seem to have + # resolved, so we are actually running this on 3.8+ + assert sys.version_info[0] >= 3 + if RUNNING_ON_MANYLINUX: + self.skipTest("Slow and not worth repeating here") + + @ignores_leakcheck + # Because we're just trying to track raw memory, not objects, and running + # the leakcheck makes an already slow test slower. + def test_untracked_memory_doesnt_increase(self): + # See https://github.com/gevent/gevent/issues/1924 + # and https://github.com/python-greenlet/greenlet/issues/328 + self._only_test_some_versions() + def f(): + return 1 + + ITER = 10000 + def run_it(): + for _ in range(ITER): + greenlet.greenlet(f).switch() + + # Establish baseline + for _ in range(3): + run_it() + + # uss: (Linux, macOS, Windows): aka "Unique Set Size", this is + # the memory which is unique to a process and which would be + # freed if the process was terminated right now. + uss_before = self.get_process_uss() + + for count in range(self.UNTRACK_ATTEMPTS): + uss_before = max(uss_before, self.get_process_uss()) + run_it() + + uss_after = self.get_process_uss() + if uss_after <= uss_before and count > 1: + break + + self.assertLessEqual(uss_after, uss_before) + + def _check_untracked_memory_thread(self, deallocate_in_thread=True): + self._only_test_some_versions() + # Like the above test, but what if there are a bunch of + # unfinished greenlets in a thread that dies? + # Does it matter if we deallocate in the thread or not? + + # First, make sure we can get useful measurements. This will + # be skipped if not. + self.get_process_uss() + + EXIT_COUNT = [0] + + def f(): + try: + greenlet.getcurrent().parent.switch() + except greenlet.GreenletExit: + EXIT_COUNT[0] += 1 + raise + return 1 + + ITER = 10000 + def run_it(): + glets = [] + for _ in range(ITER): + # Greenlet starts, switches back to us. + # We keep a strong reference to the greenlet though so it doesn't + # get a GreenletExit exception. + g = greenlet.greenlet(f) + glets.append(g) + g.switch() + + return glets + + test = self + + class ThreadFunc: + uss_before = uss_after = 0 + glets = () + ITER = 2 + def __call__(self): + self.uss_before = test.get_process_uss() + + for _ in range(self.ITER): + self.glets += tuple(run_it()) + + for g in self.glets: + test.assertIn('suspended active', str(g)) + # Drop them. + if deallocate_in_thread: + self.glets = () + self.uss_after = test.get_process_uss() + + # Establish baseline + uss_before = uss_after = None + for count in range(self.UNTRACK_ATTEMPTS): + EXIT_COUNT[0] = 0 + thread_func = ThreadFunc() + t = threading.Thread(target=thread_func) + t.start() + t.join(30) + self.assertFalse(t.is_alive()) + + if uss_before is None: + uss_before = thread_func.uss_before + + uss_before = max(uss_before, thread_func.uss_before) + if deallocate_in_thread: + self.assertEqual(thread_func.glets, ()) + self.assertEqual(EXIT_COUNT[0], ITER * thread_func.ITER) + + del thread_func # Deallocate the greenlets; but this won't raise into them + del t + if not deallocate_in_thread: + self.assertEqual(EXIT_COUNT[0], 0) + if deallocate_in_thread: + self.wait_for_pending_cleanups() + + uss_after = self.get_process_uss() + # See if we achieve a non-growth state at some point. Break when we do. + if uss_after <= uss_before and count > 1: + break + + self.wait_for_pending_cleanups() + uss_after = self.get_process_uss() + # On Windows, USS can fluctuate by tens of KB between measurements + # due to working set trimming, page table updates, etc. Allow a + # small tolerance so OS-level noise doesn't cause false failures. + # Real leaks produce MBs of growth (each iteration creates 20k + # greenlets; the greenlet Python object alone, not counting the C++ + # state it points to, is 56 bytes so 20k greenlets alone is + # slightly over a megabyte; the C++ UserGreenlet clocks in at 208 + # bytes, which adds another 4MB), so 512 KB is well below the detection + # threshold for genuine issues. + # + # 2025-06-16: We have now observed the same problem on both + # ubuntu-latest 3.14t (2948K "leak") and macos-latest 3.15t (192K + # "leak"). This was after the merge of PR #511, but that change + # should be unrelated. In particular, the ubuntu failure was after + # 30 loops and the macos failure was after only 3 --- both much + # smaller than UNTRACK_ATTEMPTS=100! We break out of the loop if we + # see a USS at the end of the loop that's smaller than at the start + # of the loop, and then we wait for pending cleanups and get the + # USS again. So this means that we think we have success, break out + # of the loop, and the pending cleanups cause our USS to grow - + # perhaps by touching pages that had been paged out? An immediately + # prior attempt of both platforms had been successful. + # + # At any rate, we need a larger tolerance than 512K, and we + # need it on all platforms, not just Windows. + tolerance = 3 * 1024 * 1024 + self.assertLessEqual(uss_after, uss_before + tolerance, + "after attempts %d" % (count,)) + + @ignores_leakcheck + # Because we're just trying to track raw memory, not objects, and running + # the leakcheck makes an already slow test slower. + def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_thread(self): + self._check_untracked_memory_thread(deallocate_in_thread=True) + + @ignores_leakcheck + # Because the main greenlets from the background threads do not exit in a timely fashion, + # we fail the object-based leakchecks. + def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main(self): + # Between Feb 10 and Feb 20 2026, this test started failing on + # Github Actions, windows 3.14t. With no relevant code changes on + # our part. Both versions were 3.14.3 (same build). The only change + # is the Github actions "Runner Image". The working one was version + # 20260202.17.1, while the updated failing version was + # 20260217.31.1. Both report the same version of the operating system + # (Microsoft Windows Server 2025 10.0.26100). + # + # Reevaluate on future runner image releases. + if WIN and RUNNING_ON_FREETHREAD_BUILD and PY314: + self.skipTest("Windows 3.14t appears to leak. No other platform does.") + self._check_untracked_memory_thread(deallocate_in_thread=False) + +if __name__ == '__main__': + __import__('unittest').main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py new file mode 100644 index 0000000..b362bf9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py @@ -0,0 +1,19 @@ +import greenlet +from . import TestCase + + +class Test(TestCase): + + def test_stack_saved(self): + main = greenlet.getcurrent() + self.assertEqual(main._stack_saved, 0) + + def func(): + main.switch(main._stack_saved) + + g = greenlet.greenlet(func) + x = g.switch() + self.assertGreater(x, 0) + self.assertGreater(g._stack_saved, 0) + g.switch() + self.assertEqual(g._stack_saved, 0) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_throw.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_throw.py new file mode 100644 index 0000000..f4f9a14 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_throw.py @@ -0,0 +1,128 @@ +import sys + + +from greenlet import greenlet +from . import TestCase + +def switch(*args): + return greenlet.getcurrent().parent.switch(*args) + + +class ThrowTests(TestCase): + def test_class(self): + def f(): + try: + switch("ok") + except RuntimeError: + switch("ok") + return + switch("fail") + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError) + self.assertEqual(res, "ok") + + def test_val(self): + def f(): + try: + switch("ok") + except RuntimeError: + val = sys.exc_info()[1] + if str(val) == "ciao": + switch("ok") + return + switch("fail") + + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError("ciao")) + self.assertEqual(res, "ok") + + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError, "ciao") + self.assertEqual(res, "ok") + + def test_kill(self): + def f(): + switch("ok") + switch("fail") + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw() + self.assertTrue(isinstance(res, greenlet.GreenletExit)) + self.assertTrue(g.dead) + res = g.throw() # immediately eaten by the already-dead greenlet + self.assertTrue(isinstance(res, greenlet.GreenletExit)) + + def test_throw_goes_to_original_parent(self): + main = greenlet.getcurrent() + + def f1(): + try: + main.switch("f1 ready to catch") + except IndexError: + return "caught" + return "normal exit" + + def f2(): + main.switch("from f2") + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + with self.assertRaises(IndexError): + g2.throw(IndexError) + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + res = g1.switch() + self.assertEqual(res, "f1 ready to catch") + res = g2.throw(IndexError) + self.assertEqual(res, "caught") + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + res = g1.switch() + self.assertEqual(res, "f1 ready to catch") + res = g2.switch() + self.assertEqual(res, "from f2") + res = g2.throw(IndexError) + self.assertEqual(res, "caught") + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + def test_non_traceback_param(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + Exception, + Exception(), + self + ) + self.assertEqual(str(exc.exception), + "throw() third argument must be a traceback object") + + def test_instance_of_wrong_type(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + Exception(), + BaseException() + ) + + self.assertEqual(str(exc.exception), + "instance exception may not have a separate value") + + def test_not_throwable(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + "abc" + ) + self.assertEqual(str(exc.exception), + "exceptions must be classes, or instances, not str") diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_tracing.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_tracing.py new file mode 100644 index 0000000..c54a910 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_tracing.py @@ -0,0 +1,298 @@ +import sys +import sysconfig +import greenlet +import unittest + +from . import TestCase +from . import PY312 + +# https://discuss.python.org/t/cpython-3-12-greenlet-and-tracing-profiling-how-to-not-crash-and-get-correct-results/33144/2 +# When build variables are available, OPT is the best way of detecting +# the build with assertions enabled. Otherwise, fallback to detecting PyDEBUG +# build. +ASSERTION_BUILD_PY312 = ( + PY312 and ( + "-DNDEBUG" not in sysconfig.get_config_var("OPT").split() + if sysconfig.get_config_var("OPT") is not None + else hasattr(sys, 'gettotalrefcount') + ), + "Broken on assertion-enabled builds of Python 3.12" +) + +class SomeError(Exception): + pass + +class GreenletTracer(object): + oldtrace = None + + def __init__(self, error_on_trace=False): + self.actions = [] + self.error_on_trace = error_on_trace + + def __call__(self, *args): + self.actions.append(args) + if self.error_on_trace: + raise SomeError + + def __enter__(self): + self.oldtrace = greenlet.settrace(self) + return self.actions + + def __exit__(self, *args): + greenlet.settrace(self.oldtrace) + + +class TestGreenletTracing(TestCase): + """ + Tests of ``greenlet.settrace()`` + """ + + def test_a_greenlet_tracing(self): + main = greenlet.getcurrent() + def dummy(): + pass + def dummyexc(): + raise SomeError() + + with GreenletTracer() as actions: + g1 = greenlet.greenlet(dummy) + g1.switch() + g2 = greenlet.greenlet(dummyexc) + self.assertRaises(SomeError, g2.switch) + + self.assertEqual(actions, [ + ('switch', (main, g1)), + ('switch', (g1, main)), + ('switch', (main, g2)), + ('throw', (g2, main)), + ]) + + def test_b_exception_disables_tracing(self): + main = greenlet.getcurrent() + def dummy(): + main.switch() + g = greenlet.greenlet(dummy) + g.switch() + with GreenletTracer(error_on_trace=True) as actions: + self.assertRaises(SomeError, g.switch) + self.assertEqual(greenlet.gettrace(), None) + + self.assertEqual(actions, [ + ('switch', (main, g)), + ]) + + def test_set_same_tracer_twice(self): + # https://github.com/python-greenlet/greenlet/issues/332 + # Our logic in asserting that the tracefunction should + # gain a reference was incorrect if the same tracefunction was set + # twice. + tracer = GreenletTracer() + with tracer: + greenlet.settrace(tracer) + + +class PythonTracer(object): + oldtrace = None + + def __init__(self): + self.actions = [] + + def __call__(self, frame, event, arg): + # Record the co_name so we have an idea what function we're in. + self.actions.append((event, frame.f_code.co_name)) + + def __enter__(self): + self.oldtrace = sys.setprofile(self) + return self.actions + + def __exit__(self, *args): + sys.setprofile(self.oldtrace) + +def tpt_callback(): + return 42 + +class TestPythonTracing(TestCase): + """ + Tests of the interaction of ``sys.settrace()`` + with greenlet facilities. + + NOTE: Most of this is probably CPython specific. + """ + + maxDiff = None + + def test_trace_events_trivial(self): + with PythonTracer() as actions: + tpt_callback() + # If we use the sys.settrace instead of setprofile, we get + # this: + + # self.assertEqual(actions, [ + # ('call', 'tpt_callback'), + # ('call', '__exit__'), + # ]) + + self.assertEqual(actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + def _trace_switch(self, glet): + with PythonTracer() as actions: + glet.switch() + return actions + + def _check_trace_events_func_already_set(self, glet): + actions = self._trace_switch(glet) + self.assertEqual(actions, [ + ('return', '__enter__'), + ('c_call', '_trace_switch'), + ('call', 'run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('return', 'run'), + ('c_return', '_trace_switch'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + def test_trace_events_into_greenlet_func_already_set(self): + def run(): + return tpt_callback() + + self._check_trace_events_func_already_set(greenlet.greenlet(run)) + + def test_trace_events_into_greenlet_subclass_already_set(self): + class X(greenlet.greenlet): + def run(self): + return tpt_callback() + self._check_trace_events_func_already_set(X()) + + def _check_trace_events_from_greenlet_sets_profiler(self, g, tracer): + g.switch() + tpt_callback() + tracer.__exit__() + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('return', 'run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + + def test_trace_events_from_greenlet_func_sets_profiler(self): + tracer = PythonTracer() + def run(): + tracer.__enter__() + return tpt_callback() + + self._check_trace_events_from_greenlet_sets_profiler(greenlet.greenlet(run), + tracer) + + def test_trace_events_from_greenlet_subclass_sets_profiler(self): + tracer = PythonTracer() + class X(greenlet.greenlet): + def run(self): + tracer.__enter__() + return tpt_callback() + + self._check_trace_events_from_greenlet_sets_profiler(X(), tracer) + + @unittest.skipIf(*ASSERTION_BUILD_PY312) + def test_trace_events_multiple_greenlets_switching(self): + tracer = PythonTracer() + + g1 = None + g2 = None + + def g1_run(): + tracer.__enter__() + tpt_callback() + g2.switch() + tpt_callback() + return 42 + + def g2_run(): + tpt_callback() + tracer.__exit__() + tpt_callback() + g1.switch() + + g1 = greenlet.greenlet(g1_run) + g2 = greenlet.greenlet(g2_run) + + x = g1.switch() + self.assertEqual(x, 42) + tpt_callback() # ensure not in the trace + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('c_call', 'g1_run'), + ('call', 'g2_run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + @unittest.skipIf(*ASSERTION_BUILD_PY312) + def test_trace_events_multiple_greenlets_switching_siblings(self): + # Like the first version, but get both greenlets running first + # as "siblings" and then establish the tracing. + tracer = PythonTracer() + + g1 = None + g2 = None + + def g1_run(): + greenlet.getcurrent().parent.switch() + tracer.__enter__() + tpt_callback() + g2.switch() + tpt_callback() + return 42 + + def g2_run(): + greenlet.getcurrent().parent.switch() + + tpt_callback() + tracer.__exit__() + tpt_callback() + g1.switch() + + g1 = greenlet.greenlet(g1_run) + g2 = greenlet.greenlet(g2_run) + + # Start g1 + g1.switch() + # And it immediately returns control to us. + # Start g2 + g2.switch() + # Which also returns. Now kick of the real part of the + # test. + x = g1.switch() + self.assertEqual(x, 42) + + tpt_callback() # ensure not in the trace + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('c_call', 'g1_run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + +if __name__ == '__main__': + unittest.main() diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_version.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_version.py new file mode 100644 index 0000000..12b55c2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_version.py @@ -0,0 +1,46 @@ +#! /usr/bin/env python + +import sys +import os +from unittest import TestCase as NonLeakingTestCase + +import greenlet + +# No reason to run this multiple times under leakchecks, +# it doesn't do anything. +class VersionTests(NonLeakingTestCase): + def test_version(self): + def find_dominating_file(name): + if os.path.exists(name): + return name + + tried = [] + here = os.path.abspath(os.path.dirname(__file__)) + for i in range(10): + up = ['..'] * i + path = [here] + up + [name] + fname = os.path.join(*path) + fname = os.path.abspath(fname) + tried.append(fname) + if os.path.exists(fname): + return fname + raise AssertionError("Could not find file " + name + "; checked " + str(tried)) + + try: + setup_py = find_dominating_file('setup.py') + except AssertionError as e: + self.skipTest("Unable to find setup.py; must be out of tree. " + str(e)) + + + invoke_setup = "%s %s --version" % (sys.executable, setup_py) + with os.popen(invoke_setup) as f: + sversion = f.read().strip() + + if not sversion or not sversion[0].isdigit(): + self.skipTest( + "setup.py --version did not return a version string " + "(likely a setuptools compatibility issue): " + + repr(sversion[:80]) + ) + + self.assertEqual(sversion, greenlet.__version__) diff --git a/venv/lib/python3.11/site-packages/greenlet/tests/test_weakref.py b/venv/lib/python3.11/site-packages/greenlet/tests/test_weakref.py new file mode 100644 index 0000000..05a38a7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/greenlet/tests/test_weakref.py @@ -0,0 +1,35 @@ +import gc +import weakref + + +import greenlet +from . import TestCase + +class WeakRefTests(TestCase): + def test_dead_weakref(self): + def _dead_greenlet(): + g = greenlet.greenlet(lambda: None) + g.switch() + return g + o = weakref.ref(_dead_greenlet()) + gc.collect() + self.assertEqual(o(), None) + + def test_inactive_weakref(self): + o = weakref.ref(greenlet.greenlet()) + gc.collect() + self.assertEqual(o(), None) + + def test_dealloc_weakref(self): + seen = [] + def worker(): + try: + greenlet.getcurrent().parent.switch() + finally: + seen.append(g()) + g = greenlet.greenlet(worker) + g.switch() + g2 = greenlet.greenlet(lambda: None, g) + g = weakref.ref(g2) + g2 = None + self.assertEqual(seen, [None]) diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/METADATA b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/METADATA new file mode 100644 index 0000000..0dbb8b8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/METADATA @@ -0,0 +1,79 @@ +Metadata-Version: 2.4 +Name: playwright +Version: 1.64.1 +Summary: A high-level API to automate web browsers +Author: Microsoft Corporation +License-Expression: Apache-2.0 +Project-URL: homepage, https://github.com/Microsoft/playwright-python +Project-URL: Release notes, https://github.com/microsoft/playwright-python/releases +Classifier: Topic :: Software Development :: Testing +Classifier: Topic :: Internet :: WWW/HTTP :: Browsers +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Operating System :: OS Independent +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: pyee<15,>=13 +Requires-Dist: greenlet<4.0.0,>=3.1.1 +Dynamic: license-file + +# 🎭 [Playwright](https://playwright.dev) for Python [![PyPI](https://badge.fury.io/py/playwright.svg)](https://pypi.python.org/pypi/playwright/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) + +Playwright is a Python library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) browsers with a single API. Playwright delivers automation that is **ever-green**, **capable**, **reliable** and **fast**. [See how Playwright is better](https://playwright.dev/python). + +| | Linux | macOS | Windows | +| :--- | :---: | :---: | :---: | +| Chromium 156.0.8078.4 | ✅ | ✅ | ✅ | +| WebKit 27.2 | ✅ | ✅ | ✅ | +| Firefox 157.0 | ✅ | ✅ | ✅ | + +## Documentation + +[https://playwright.dev/python/docs/intro](https://playwright.dev/python/docs/intro) + +## API Reference + +[https://playwright.dev/python/docs/api/class-playwright](https://playwright.dev/python/docs/api/class-playwright) + +## Example + +```py +from playwright.sync_api import sync_playwright + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + browser = browser_type.launch() + page = browser.new_page() + page.goto('http://playwright.dev') + page.screenshot(path=f'example-{browser_type.name}.png') + browser.close() +``` + +```py +import asyncio +from playwright.async_api import async_playwright + +async def main(): + async with async_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + browser = await browser_type.launch() + page = await browser.new_page() + await page.goto('http://playwright.dev') + await page.screenshot(path=f'example-{browser_type.name}.png') + await browser.close() + +asyncio.run(main()) +``` + +## Other languages + +More comfortable in another programming language? [Playwright](https://playwright.dev) is also available in +- [Node.js (JavaScript / TypeScript)](https://playwright.dev/docs/intro), +- [.NET](https://playwright.dev/dotnet/docs/intro), +- [Java](https://playwright.dev/java/docs/intro). diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/RECORD b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/RECORD new file mode 100644 index 0000000..1cbdbf2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/RECORD @@ -0,0 +1,266 @@ +../../../bin/playwright,sha256=-Q3H1EliY5xFNAvrpeUHrY1LCD9gMmWTNB_gRZJY6MI,207 +playwright-1.64.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +playwright-1.64.1.dist-info/METADATA,sha256=XF50ZzCceroc6IZpROPegXg6T132f-4XM6TrXiSGhxc,3327 +playwright-1.64.1.dist-info/RECORD,, +playwright-1.64.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +playwright-1.64.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +playwright-1.64.1.dist-info/entry_points.txt,sha256=G-lS5tnQw21PHcXKbZFGy81lczindXIDUejIefH8VoY,130 +playwright-1.64.1.dist-info/licenses/LICENSE,sha256=f6sUYbQZcP83bxyTA6Y3B2v6rrcc0S3TocRKr1mhork,11399 +playwright-1.64.1.dist-info/top_level.txt,sha256=oYiz5-Zb4y23ANZot2i3lMsK-BAiwXqTlQ4hytLJYuE,11 +playwright/__init__.py,sha256=BsOs2t0T4oE_y5WtkdU-44hcK19HO-zMRaV52c7q1ws,805 +playwright/__main__.py,sha256=-NZUiThJKet2OjTBTjzsMsrNL5L8swBZND4PC5tW3cc,1064 +playwright/__pycache__/__init__.cpython-311.pyc,, +playwright/__pycache__/__main__.cpython-311.pyc,, +playwright/__pycache__/_repo_version.cpython-311.pyc,, +playwright/_impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +playwright/_impl/__pycache__/__init__.cpython-311.pyc,, +playwright/_impl/__pycache__/_api_structures.cpython-311.pyc,, +playwright/_impl/__pycache__/_artifact.cpython-311.pyc,, +playwright/_impl/__pycache__/_assertions.cpython-311.pyc,, +playwright/_impl/__pycache__/_async_base.cpython-311.pyc,, +playwright/_impl/__pycache__/_browser.cpython-311.pyc,, +playwright/_impl/__pycache__/_browser_context.cpython-311.pyc,, +playwright/_impl/__pycache__/_browser_type.cpython-311.pyc,, +playwright/_impl/__pycache__/_cdp_session.cpython-311.pyc,, +playwright/_impl/__pycache__/_clock.cpython-311.pyc,, +playwright/_impl/__pycache__/_connection.cpython-311.pyc,, +playwright/_impl/__pycache__/_console_message.cpython-311.pyc,, +playwright/_impl/__pycache__/_credentials.cpython-311.pyc,, +playwright/_impl/__pycache__/_debugger.cpython-311.pyc,, +playwright/_impl/__pycache__/_dialog.cpython-311.pyc,, +playwright/_impl/__pycache__/_disposable.cpython-311.pyc,, +playwright/_impl/__pycache__/_download.cpython-311.pyc,, +playwright/_impl/__pycache__/_driver.cpython-311.pyc,, +playwright/_impl/__pycache__/_element_handle.cpython-311.pyc,, +playwright/_impl/__pycache__/_errors.cpython-311.pyc,, +playwright/_impl/__pycache__/_event_context_manager.cpython-311.pyc,, +playwright/_impl/__pycache__/_fetch.cpython-311.pyc,, +playwright/_impl/__pycache__/_file_chooser.cpython-311.pyc,, +playwright/_impl/__pycache__/_form_data.cpython-311.pyc,, +playwright/_impl/__pycache__/_frame.cpython-311.pyc,, +playwright/_impl/__pycache__/_glob.cpython-311.pyc,, +playwright/_impl/__pycache__/_greenlets.cpython-311.pyc,, +playwright/_impl/__pycache__/_har_router.cpython-311.pyc,, +playwright/_impl/__pycache__/_helper.cpython-311.pyc,, +playwright/_impl/__pycache__/_impl_to_api_mapping.cpython-311.pyc,, +playwright/_impl/__pycache__/_input.cpython-311.pyc,, +playwright/_impl/__pycache__/_internal_structures.cpython-311.pyc,, +playwright/_impl/__pycache__/_js_handle.cpython-311.pyc,, +playwright/_impl/__pycache__/_json_pipe.cpython-311.pyc,, +playwright/_impl/__pycache__/_local_utils.cpython-311.pyc,, +playwright/_impl/__pycache__/_locator.cpython-311.pyc,, +playwright/_impl/__pycache__/_map.cpython-311.pyc,, +playwright/_impl/__pycache__/_network.cpython-311.pyc,, +playwright/_impl/__pycache__/_object_factory.cpython-311.pyc,, +playwright/_impl/__pycache__/_page.cpython-311.pyc,, +playwright/_impl/__pycache__/_path_utils.cpython-311.pyc,, +playwright/_impl/__pycache__/_playwright.cpython-311.pyc,, +playwright/_impl/__pycache__/_screencast.cpython-311.pyc,, +playwright/_impl/__pycache__/_selectors.cpython-311.pyc,, +playwright/_impl/__pycache__/_set_input_files_helpers.cpython-311.pyc,, +playwright/_impl/__pycache__/_signature.cpython-311.pyc,, +playwright/_impl/__pycache__/_str_utils.cpython-311.pyc,, +playwright/_impl/__pycache__/_stream.cpython-311.pyc,, +playwright/_impl/__pycache__/_sync_base.cpython-311.pyc,, +playwright/_impl/__pycache__/_tracing.cpython-311.pyc,, +playwright/_impl/__pycache__/_transport.cpython-311.pyc,, +playwright/_impl/__pycache__/_video.cpython-311.pyc,, +playwright/_impl/__pycache__/_waiter.cpython-311.pyc,, +playwright/_impl/__pycache__/_web_error.cpython-311.pyc,, +playwright/_impl/__pycache__/_web_storage.cpython-311.pyc,, +playwright/_impl/__pycache__/_webmcp.cpython-311.pyc,, +playwright/_impl/__pycache__/_writable_stream.cpython-311.pyc,, +playwright/_impl/__pyinstaller/__init__.py,sha256=WowM_rLYTtQoev3sJP_Oxg0e2Ce5Kv372CsjUf-uHBU,691 +playwright/_impl/__pyinstaller/__pycache__/__init__.cpython-311.pyc,, +playwright/_impl/__pyinstaller/__pycache__/hook-playwright.async_api.cpython-311.pyc,, +playwright/_impl/__pyinstaller/__pycache__/hook-playwright.sync_api.cpython-311.pyc,, +playwright/_impl/__pyinstaller/hook-playwright.async_api.py,sha256=1KjEVVSzulXoEC8D9Xa6tupKv7cM3GAJVHHVBONKEBQ,695 +playwright/_impl/__pyinstaller/hook-playwright.sync_api.py,sha256=1KjEVVSzulXoEC8D9Xa6tupKv7cM3GAJVHHVBONKEBQ,695 +playwright/_impl/_api_structures.py,sha256=uYVIGInooNaQyK0XVrSOqRL4Ja0skwR8hmn8LebUWmg,5247 +playwright/_impl/_artifact.py,sha256=NZbbcRNmJIoZPGoPG-hUcDSYe3J0yUnSzL48bTGYrHE,2702 +playwright/_impl/_assertions.py,sha256=rCSG6NGnkuVDpeDuXlNDq4cjFNzULuS1TmxvEEGAhHQ,36226 +playwright/_impl/_async_base.py,sha256=LZ4J0RBf8sCJSmOGAtxTnkLpIssF9anEsckDi7xwfJc,3212 +playwright/_impl/_browser.py,sha256=qYFsjfmEuotngDV8ILF36D8n9LNDCOT-fbz03jUTWo0,10537 +playwright/_impl/_browser_context.py,sha256=W5yOxNipFxPvcStBdfgq8fXCyIhXiWmSHLRMwxZ2YfU,27093 +playwright/_impl/_browser_type.py,sha256=4p6i2xPDYMOuA4T25R90L3389x7_ETin_U0v2uAu7n8,14524 +playwright/_impl/_cdp_session.py,sha256=v05nvVeQ4cWb-6_QYAlnwjx2LQbKbQQZ-JIbu3wy9Ok,1625 +playwright/_impl/_clock.py,sha256=ScYexA7fp-3cJkxvRhBRVDH_83bzAydqPbN6OiPi_kA,3040 +playwright/_impl/_connection.py,sha256=sLUb4LKZcNTuWG8enR8P93Yt7i1yuM4TjAb0P7-65Us,27107 +playwright/_impl/_console_message.py,sha256=0h1jn-G3wSm6OpkcIHbR7TWx2bmpFC-QijNBJF69gds,3123 +playwright/_impl/_credentials.py,sha256=2o-REnC6oZT26qAsP3xO2zVtAMWtD0YbCJMjoIftUlI,2068 +playwright/_impl/_debugger.py,sha256=GVhKoeYaaJYAFnkiwA8ZlSKEVWfGvHB9iFWdJc5O9kg,1978 +playwright/_impl/_dialog.py,sha256=Gui_nRK2lKTHN9buD_7zzO4Tnv7Ef_t-IvDuJT-hSKo,2080 +playwright/_impl/_disposable.py,sha256=JBGypfd9iJCK1qnnYpAXpqY4DFYBXLBQO9ry-gAyTqE,2925 +playwright/_impl/_download.py,sha256=Bk1oB9grXc2AlKLxuRDB0zY2O_E1IA7hlP7x7DunXgQ,1977 +playwright/_impl/_driver.py,sha256=mhVGneSw4VdzFp0y3xKippQpiosD7mhlXFBFMqJ9Tqo,1399 +playwright/_impl/_element_handle.py,sha256=xXYWGLHwjZwNbD4QT5e-8Mxy9yEUwq2Q7-C5woQ_KwA,15309 +playwright/_impl/_errors.py,sha256=SUB71hZKtWP4KyvzeGOiZVVQcVRNyr7b9766i-G2GM0,1950 +playwright/_impl/_event_context_manager.py,sha256=VeoPq1oXrLs_b3l2EM8gp378vHti6MGbrFOhOPOccSc,1044 +playwright/_impl/_fetch.py,sha256=Knio5ZjdktElDQtLHyyx26Sh8XcgbMjVUN2fDBC04cs,22154 +playwright/_impl/_file_chooser.py,sha256=tR_VFihVgmCMhVGBL7SQq46iaVKLK6JVeu_w3sjDMqA,1859 +playwright/_impl/_form_data.py,sha256=qfi4xrolCd4wuE6QYjOOMRFN5k3jW9IEsk86fkKaCgw,1205 +playwright/_impl/_frame.py,sha256=xxQAIDTlQBD7STzKRJazvgnylZRbE3s0qArQOAEd1kg,31868 +playwright/_impl/_glob.py,sha256=r5J8HMGjPhIJQiTKlhWJZM4setn0tN46xg4W4Lr5-Ic,2269 +playwright/_impl/_greenlets.py,sha256=V1KnmdReu5EH1JiHmAN66J40VXnGbOs62U-GY4qaDSk,3158 +playwright/_impl/_har_router.py,sha256=NvmXmxoXcY8mmK250APQeRGynToQUDumFcgqvRxxUU8,4497 +playwright/_impl/_helper.py,sha256=usdg1XXB1IK56NrbCMXL1JltQKXxsiv8sQ07I2UTMXA,19779 +playwright/_impl/_impl_to_api_mapping.py,sha256=M1W9bjzJWbTHO30Z1l03v75w5o0wGoHM3t4fpgjBQRQ,5473 +playwright/_impl/_input.py,sha256=AEYlZNGaW5xGrm5f6BvfDJVX194WIC3t8gsHUYOcJHU,3753 +playwright/_impl/_internal_structures.py,sha256=cM8kOnL8YvLr22cc60zkbmzlOFU-bWJP0LfhKiEth2U,3028 +playwright/_impl/_js_handle.py,sha256=lhmHdnX_hVyxf_8BuSFtCVR_Gyfl1ivv3dnDIMQ_t5Q,10772 +playwright/_impl/_json_pipe.py,sha256=1oObQNs87YdkbnLI5VxQgiaxE1DKS_ucv_xOwe-CZGk,2616 +playwright/_impl/_local_utils.py,sha256=7h57N3f_RFFGdG7_iOO8ZZkBByi-dEw3AtzKTidovTc,3393 +playwright/_impl/_locator.py,sha256=jW0bBu4dD5JlemHNxo5A8I_uHWF9lDGSR37rbhuNwx8,32970 +playwright/_impl/_map.py,sha256=YGCGtT7cKWBaX_CyIUffxkDjjGUInfeQBjrtnLib-qc,1045 +playwright/_impl/_network.py,sha256=CR2CsOqq08jCfsvYp6xhZI5JQ_utIoCO8gw5J2f1-40,36456 +playwright/_impl/_object_factory.py,sha256=V32yrXXtWIc7B4w6hbuMnG40cTcUAjLhwtebQWNH5RM,4406 +playwright/_impl/_page.py,sha256=zevcIIXa1SgKwt5CPv1-u70bmv0VkdCjRQJ6HrzXYtI,56228 +playwright/_impl/_path_utils.py,sha256=LAw06MFOBNNQBE5ezKTYBLTF-RkalMRQ9Y5qP52LzRw,958 +playwright/_impl/_playwright.py,sha256=llbxT8HVIbHLVJJVmWRtWqAHIbFktrJjJWcncSNwyCU,2091 +playwright/_impl/_screencast.py,sha256=_0_8JTCvxWdr2tZW71Qta8UOpaKGsjuX4BVA58f0RUE,6067 +playwright/_impl/_selectors.py,sha256=tXtMxs2_V07lExP42Zeu4UY6yRJkOgBR9R1fbiY_BbY,2636 +playwright/_impl/_set_input_files_helpers.py,sha256=oc6OWkEDcZ5qnPVpqRkuX0jf35EZnPVy0Cp3FdDUceA,5605 +playwright/_impl/_signature.py,sha256=XWGr64yk-zLgm_IT79-urxXbhz2d3hMFuO1iRishDH0,1261 +playwright/_impl/_str_utils.py,sha256=KFYoq-pDxfBcgCA4NEMNylU0KJNUgELg6sOslzm2upE,2793 +playwright/_impl/_stream.py,sha256=iOYBs1xinVtaFmf_2-QMoatwnRWcRSZ2dzcMii0PBXc,1735 +playwright/_impl/_sync_base.py,sha256=lkbR6uOkop9UDrQnDyxhyl3RIL8aACoF_gZbl7UeDKM,4514 +playwright/_impl/_tracing.py,sha256=u9M_C_2E2cuTgoeIfbVq069LGejsxR8GjTOS3x2qhDU,10264 +playwright/_impl/_transport.py,sha256=cM6YP8XiDe47ZWaQuIxc9Velzqyz2eMnwVKzFdQKDpQ,6885 +playwright/_impl/_video.py,sha256=1YzIwILwu4wFp3RVoJyc3iqqcFpOP27pOEX9OYB4MJM,2074 +playwright/_impl/_waiter.py,sha256=o2ZgcM-wTc5zBu78apqu-SU-PAYIe-z7kx8WgkzOICM,6493 +playwright/_impl/_web_error.py,sha256=Vd-CGb5Bks-j_T2ajTMFYaVHPaSL0FBamlFffCwQNPo,1420 +playwright/_impl/_web_storage.py,sha256=I6wmgiY3TECS46EHnNEQQgvjB5uo0bCMZIVR7FX29vA,2039 +playwright/_impl/_webmcp.py,sha256=Wr1T_dsbLxP8lEYBeeiFuTIFlxKuu4q65W1cehlrP7w,1655 +playwright/_impl/_writable_stream.py,sha256=Jf6ppPdyUwD-eNCWxZhDr5ejoTF1NZ_GGGK999eQcL8,1472 +playwright/_repo_version.py,sha256=Inbndvc_wrcUUQRRefC0EIOW63y6tHOhCm8DmtFMgYA,530 +playwright/async_api/__init__.py,sha256=2lS9YGmE1hrDqOcDEOi9MadI1Suzo3k_u3xmRBKA120,8124 +playwright/async_api/__pycache__/__init__.cpython-311.pyc,, +playwright/async_api/__pycache__/_context_manager.cpython-311.pyc,, +playwright/async_api/__pycache__/_generated.cpython-311.pyc,, +playwright/async_api/_context_manager.py,sha256=TWWR-Qa9mhmoORyAVVnqinX6RX43oxxrrGUld_Q6APo,2041 +playwright/async_api/_generated.py,sha256=LtTSVtBN6JLQm2enEQ3VU_EM9tryC9tIx-bDjv98JfA,972936 +playwright/driver/LICENSE,sha256=WIjbuaHSsY8sPmxfavGzneZYNytAKgV3sAJ3fxTGKs4,157609 +playwright/driver/README.md,sha256=lEEFmEydUlEnGeCLSrOebdQ2bfJR4ocjXi5bGzXJVTU,36 +playwright/driver/node,sha256=4y4tFggc9kT4g5KTQogIbnZiUp0NQyd8NPeq0iB8-vs,126879696 +playwright/driver/package/LICENSE,sha256=RYc9AKDdJDWW3rSqI7JJOz0fBnGSG_JTjqQx1zgCIOs,11601 +playwright/driver/package/NOTICE,sha256=bWAhkRh7NbmwHSz_oByEacLI2d6KlvG_ho4PJk9RyB0,254 +playwright/driver/package/README.md,sha256=0W_3-OOPofyVT2la1VHNwRjZyb3V89gyA5AfTDF9fX4,120 +playwright/driver/package/ThirdPartyNotices.txt,sha256=pUnTKbrYgG_iefDsrg_AJw3sfnwtyOwQ-Q45T3wysUQ,676 +playwright/driver/package/bin/install_media_pack.ps1,sha256=vCDwSbrfAh46U720FWYDyqXvSCyamiUfknYF5nakQXE,174 +playwright/driver/package/bin/install_webkit_wsl.ps1,sha256=vGG-2jeoCO7yfTnOaEuqeS9tF3N6JaQysahNO1kauBU,1464 +playwright/driver/package/bin/reinstall_chrome_beta_linux.sh,sha256=3Iwiud3nZYPOzihXV1uICUKZYV8xNqPFmBSXEMRd7jo,1217 +playwright/driver/package/bin/reinstall_chrome_beta_mac.sh,sha256=qS6utOvPVjOk6cmBSB4UyAdk_IG2_LwFZTmOUP8xlDM,563 +playwright/driver/package/bin/reinstall_chrome_beta_win.ps1,sha256=ttnkR0L-r7mNXtxW-iAEKUmXPPMNFBocW0-zhAlpZ8U,957 +playwright/driver/package/bin/reinstall_chrome_stable_linux.sh,sha256=3HKrNWekzkO9g1PU7PQfHZwD6HAWk2fNPtkEOqv2fdE,1212 +playwright/driver/package/bin/reinstall_chrome_stable_mac.sh,sha256=u0OpbBkqbMgVJK3gJkaG__eAnAGaiffbCphJY1h5w74,519 +playwright/driver/package/bin/reinstall_chrome_stable_win.ps1,sha256=PZbE4vTruz-tRM87pY2c-Qrxyfswc-OyfUWnTPAFUVs,923 +playwright/driver/package/bin/reinstall_msedge_beta_linux.sh,sha256=ELJvNdvCAAuU7fEXLYA6KcWHOk641SOvvGh5l7ZCdn4,1494 +playwright/driver/package/bin/reinstall_msedge_beta_mac.sh,sha256=ADbXOLyQ1oYQDYHzKLoAA78Hhj9VALeSyNo5HRSXUPo,377 +playwright/driver/package/bin/reinstall_msedge_beta_win.ps1,sha256=sXPDeKWfDMOAM87nAt4fRPCSdsmyJw0aZQ6-Zu4xJDM,874 +playwright/driver/package/bin/reinstall_msedge_dev_linux.sh,sha256=AOSUfnpPgznG2wnIeXbYCFg8B_kXnkqnaU2IYUuC_pU,1489 +playwright/driver/package/bin/reinstall_msedge_dev_mac.sh,sha256=b4meOzEfdCwkyjf_njIpjoKQUU_8FL9YjMUVVqKdpMA,372 +playwright/driver/package/bin/reinstall_msedge_dev_win.ps1,sha256=Rvf5jIHLCr5ofX9KWv6puJejjSpt1vZmL72Ef3oivVE,869 +playwright/driver/package/bin/reinstall_msedge_stable_linux.sh,sha256=7AoxpfCcVl4QhHOhhQiVpEWKsAxxOsHeOqIAGRLkZo8,1507 +playwright/driver/package/bin/reinstall_msedge_stable_mac.sh,sha256=OvHok1J_8iV6otvDslmgU7kvRcPq2xPjjrpG6HkCZV0,371 +playwright/driver/package/bin/reinstall_msedge_stable_win.ps1,sha256=QLtfy_zru-LOQ8bnNufF3u_FvgO26caRRCPAbsH1kb4,856 +playwright/driver/package/browsers.json,sha256=LP1Bn3GsZHXYHhMB-obDTPKiprrioNpLWisYMQIWtr8,1135 +playwright/driver/package/cli.js,sha256=fEb3mTY-7j4Lt6fXyHCpIxahYBv2_aeAtlbfQglUKqk,867 +playwright/driver/package/index.d.ts,sha256=eo7BCwg063GD5L_Nkpg4rHdYOCjjQyEbtzZ20eR_bwE,634 +playwright/driver/package/index.js,sha256=pY-yzsQpPn3Pc8WKF5iYpPOYZmZFGhnaTnMfo69jJls,697 +playwright/driver/package/index.mjs,sha256=PTbiEy08i2I5fSPwu0F3M2qu5QMgOuKiyQ--tb8zF90,1063 +playwright/driver/package/lib/bootstrap.js,sha256=Od1kzJRyJ10bJcFw_voqmDU7zKWYt0F4j6rC5uFfxzc,5739 +playwright/driver/package/lib/coreBundle.js,sha256=gpUhtn7XvpDH1tYJj_OfIkFWLUfkrXzKsTcQkkpWAB0,3585406 +playwright/driver/package/lib/entry/cliDaemon.js,sha256=Bi8lb7Kae_kXk6AUucPx-c8txrEuQKii67qIpjCuFqA,197 +playwright/driver/package/lib/entry/dashboardApp.js,sha256=IJlixuru8RYp8ibWoqsz7HokgHVCQrbhlZCp5utx0SY,108 +playwright/driver/package/lib/entry/mcp.js,sha256=zIfcACDLQcrrvwT1DkK8eDWwM6bYDpw_Cj0LPb3zQcc,442 +playwright/driver/package/lib/entry/oopBrowserDownload.js,sha256=h1Z829Z90vULsjLUfTeEcB7OfIHjnAKHu30bRmtozwY,120 +playwright/driver/package/lib/package.js,sha256=n5OOHUSg3SMbFmDKYvfa6WSFm3nL3DcLhULF03XKPAU,2112 +playwright/driver/package/lib/server/chromium/appIcon.png,sha256=a6mU8Fxc8Y4i2ba--YmPvexq1NTXf1GH0GCctC1y0B4,16565 +playwright/driver/package/lib/server/electron/loader.js,sha256=rekVnJaRWXFAuyZYuF9QVf8_p7xcDittVTL5-9Rt_ao,5407 +playwright/driver/package/lib/serverRegistry.js,sha256=OAJfCPM1cFsJm58Yi_RbWQilanIoJf6JWx6Q9W1VlbY,57039 +playwright/driver/package/lib/serverRegistry.js.LICENSE,sha256=MJim78vBF1LL32B1-mVID1Rw94yNmB-rnAeW3JpBzv4,2982 +playwright/driver/package/lib/tools/cli-client/channelSessions.js,sha256=a5xhPoc0D0itcYKt3XSVqP8_vlX-sbvQzgT1nxVS4SY,7087 +playwright/driver/package/lib/tools/cli-client/cli.js,sha256=P5aU5GHvDW66sGj989qY4fO8581NLDLmZ_LQ-yGMZHI,225 +playwright/driver/package/lib/tools/cli-client/help.json,sha256=kiswL99pfzLDfZrE3kU8mPifjt_BWx0xD600cQehK_E,44187 +playwright/driver/package/lib/tools/cli-client/minimist.js,sha256=lDBInrwG_QIjHcz-hEUW90PoEqmsT-jmdIpncYc4hRw,4109 +playwright/driver/package/lib/tools/cli-client/output.js,sha256=3lcB1tunezx3Rxw-ga2QHf0akSLTjyX1wG-hNW3AArY,12359 +playwright/driver/package/lib/tools/cli-client/program.js,sha256=wi0tEchi3xmCKNlhLmv21C2GB50ERfFR8O-pom0DQYk,16554 +playwright/driver/package/lib/tools/cli-client/registry.js,sha256=SXbNWys6JVcswaS18FWnVtrNTMGxnUiYfuMlNCjnWbg,6768 +playwright/driver/package/lib/tools/cli-client/session.js,sha256=jD-a_z-rU3dxy1YOG60-QX4WszFbej3BCjBfrso_YCQ,9650 +playwright/driver/package/lib/tools/dashboard/appIcon.png,sha256=OAN7zxjH-blQ2i4aLYgA04-ydjfM2ik9ODaKNwS2fxg,18945 +playwright/driver/package/lib/tools/skills/playwright-cli/SKILL.md,sha256=Sb68S2UtWIYOkdZHj6RGZF40m6hVZmfMAnAmRdf_3gs,15662 +playwright/driver/package/lib/tools/skills/playwright-cli/references/element-attributes.md,sha256=vxmqRnHgpQoP76nHkPORJOgDBg7v45UEK3I8Kfy3-qI,665 +playwright/driver/package/lib/tools/skills/playwright-cli/references/playwright-tests.md,sha256=xbBxn-8s_f9QvXRAUbL5r8EZd120tI6bKILBzziGeXw,1690 +playwright/driver/package/lib/tools/skills/playwright-cli/references/pr-attachments.md,sha256=JLEY50WYvnLnPiT-Ij0pW5CZyFsYYKfI9uVpDJA4-io,2830 +playwright/driver/package/lib/tools/skills/playwright-cli/references/request-mocking.md,sha256=VOgByWY_wrbWjOsFjLHDYHJMJJn0Ksx4UqaOg-W183w,2182 +playwright/driver/package/lib/tools/skills/playwright-cli/references/running-code.md,sha256=MClWntbdrv_AuHut3pZ-_cKXOxZ38HiDAJgUbrO3jr4,5913 +playwright/driver/package/lib/tools/skills/playwright-cli/references/session-management.md,sha256=pPFicfldPux7xW5lnYrbuPatfWA6b6Fe_c3QcaUMbOM,5929 +playwright/driver/package/lib/tools/skills/playwright-cli/references/storage-state.md,sha256=msR_muShrtzSB3-Kyasbpr7hliy4P_Ua3NNvttg7XsY,5198 +playwright/driver/package/lib/tools/skills/playwright-cli/references/test-generation.md,sha256=J-yxHvZHmZvXSGaVuxqsw2ggl-a7V0KATLbIdpwrU2Q,16070 +playwright/driver/package/lib/tools/skills/playwright-cli/references/tracing.md,sha256=73PnG-55LjQd-c1PYFKckpreSLU4rHbgM_TyCEPo29o,3456 +playwright/driver/package/lib/tools/skills/playwright-cli/references/video-recording.md,sha256=HIALiXgky1Q5TDQmXY6__btTCt_I1hLF15i2Z6F5n0g,9388 +playwright/driver/package/lib/tools/skills/playwright-component-testing/SKILL.md,sha256=YPV7MXZ9ViwwzFMyCBQYwpRfTZyZjvpuCRkTM2VN1Ck,10404 +playwright/driver/package/lib/tools/skills/playwright-component-testing/references/gallery-spec.md,sha256=DKJd3mES6zefVuthhYW2TcLf0Twvf5Vqx-q712pokSs,6199 +playwright/driver/package/lib/tools/skills/playwright-component-testing/references/migration.md,sha256=vDozb3lwu3RV-lo4-TfkWzX7DBjWw7nzeRak_3V7esw,5031 +playwright/driver/package/lib/tools/skills/playwright-component-testing/references/react.md,sha256=VK-cwc221MzbjoEcCrspuWDVyTBPTffR3Pm43z-XMQI,3115 +playwright/driver/package/lib/tools/skills/playwright-component-testing/references/typing.md,sha256=xRECW1ybAtf0kDSlYTyLR4GTv2DJDqbd2z-vnGmFyCs,6883 +playwright/driver/package/lib/tools/skills/playwright-component-testing/references/vue.md,sha256=0bEp8SbHAHAqPACtjrbIaJi8lBwpplbBGR63IwZQv4s,3374 +playwright/driver/package/lib/tools/skills/playwright-trace/SKILL.md,sha256=x20oJEwFpkdPYDAhMSfNYiUyCLBkMOojeFdvBmnbLJo,4862 +playwright/driver/package/lib/tools/utils/extension.js,sha256=QErhnlenx8XtPm7PnfJqP8-VhnJQcy-VSCMHjMuHnmM,4689 +playwright/driver/package/lib/tools/utils/socketConnection.js,sha256=lzwr6nCh8URfRnP5np-jUeUKLsn9k9c4GyxBu03nZN0,3317 +playwright/driver/package/lib/utilsBundle.js,sha256=dfFDHqfuALKykaCnLU-hwHrgTZmfIaAM0DuPRUpYQPw,3085095 +playwright/driver/package/lib/utilsBundle.js.LICENSE,sha256=E2K39BPAgIOvU3OEFiq2mjI9AZ_ibgos14U2-v1rnL8,106591 +playwright/driver/package/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css,sha256=IL6CSoKDK2LMBGQtMdo8YJ_SgLI_HNyx5iLE9AqsWrg,6251 +playwright/driver/package/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js,sha256=VrnGpQwJ36wo7qXsPnbIroWiYAmyDrrJkolZzzwzErs,307374 +playwright/driver/package/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf,sha256=Dx1SGZNOlug7jbFi1gtNjAm13h59OAMcuv5KPA8oick,80340 +playwright/driver/package/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg,sha256=PzdC0eeMDbbC01MSUPcMMGde5TEN4y-om--vc4xU79A,7488 +playwright/driver/package/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg,sha256=fqtf_cJZA7ozpyAvf_-hmZ8RnHvkWjnp5wPBGJBPkXo,9280 +playwright/driver/package/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg,sha256=8xHAdLD6mZ_Na3NEEwH7VO6HpI_zvwXzGGR5KBEEdto,9143 +playwright/driver/package/lib/vite/dashboard/assets/index-AwgwcYie.css,sha256=gtBvKRp9CGDhseGAOrby5JxjdUF3mjcWhlLhCwYrZ_k,139111 +playwright/driver/package/lib/vite/dashboard/assets/index-hRvO4yU2.js,sha256=OgXtCwJkQqFFmVLfJbmoNtT6GpF4OM9vk1fa78DkIoU,336491 +playwright/driver/package/lib/vite/dashboard/assets/safari-na3_-uQk.svg,sha256=gP1yyl64VF8ZgSd7MfT5KVHUadTv69B45AhrZ2_lF9w,5254 +playwright/driver/package/lib/vite/dashboard/index.html,sha256=ICkDqtZgQGBLwaDIVhXdpL_H5vSkZ05g2e2k1E9vrqc,1078 +playwright/driver/package/lib/vite/dashboard/playwright-logo.svg,sha256=URUkocFFgZMDoY-3GHCkMHw-dA-elyL3aGorQrCZW2w,5645 +playwright/driver/package/lib/vite/htmlReport/index.html,sha256=znwJgCCg65zhDt1BYkXqbaUN_qqxjf0KIXaXnSJkO-Y,464 +playwright/driver/package/lib/vite/htmlReport/report.css,sha256=2YokS_lQMzreODsQnpfTlvlw9lCyDy5e9ezCdAIF3_c,100452 +playwright/driver/package/lib/vite/htmlReport/report.js,sha256=Ds9TKc408LgJdTOSuHFVIcMO60biT3dJ0lN8BqpbzqQ,415889 +playwright/driver/package/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css,sha256=IL6CSoKDK2LMBGQtMdo8YJ_SgLI_HNyx5iLE9AqsWrg,6251 +playwright/driver/package/lib/vite/recorder/assets/codeMirrorModule-DK72OMRi.js,sha256=RMwuhBP5QbTOE2-uIpzjdaC_4V6fEXLcDesCMHkRSfQ,307374 +playwright/driver/package/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf,sha256=Dx1SGZNOlug7jbFi1gtNjAm13h59OAMcuv5KPA8oick,80340 +playwright/driver/package/lib/vite/recorder/assets/index-BZpYJZ-P.css,sha256=zCRWO5byhmFLiPdNpBPpmE3iYyJ5ccTN-r4fc74JVRQ,85502 +playwright/driver/package/lib/vite/recorder/assets/index-Cn0mmDuv.js,sha256=tXrdnhCG0rcFimb3ehwZuov7zx92gxVl52YoksaNYBo,358646 +playwright/driver/package/lib/vite/recorder/index.html,sha256=gzj6p9WFbXJUAKHBqjdUG_Ra0OnKA5GwXWIhGKR9W8A,1078 +playwright/driver/package/lib/vite/recorder/playwright-logo.svg,sha256=awpDZ73qsQmVvCOSePBMaMEOSK2-wV55ngGQmg1m3Lk,5033 +playwright/driver/package/lib/vite/traceViewer/assets/codeMirrorModule-BEB2e8M3.js,sha256=DaddIoq8HYuUCqBOwIZXDaznsueyzMX0vBbwXnm9t9c,307393 +playwright/driver/package/lib/vite/traceViewer/assets/defaultSettingsView-D-g2vrvf.js,sha256=VUve-RDRjnYxGPEiFme4Ce2vukbinj-N5Lnc8hxrfXk,667649 +playwright/driver/package/lib/vite/traceViewer/assets/urlMatch-L3liM589.js,sha256=G8YyHjLRHDGkUrx1iW8LNE5hUzIYVNnxZYgYvn-EXBY,2510 +playwright/driver/package/lib/vite/traceViewer/assets/xtermModule-B794rD2Z.js,sha256=fzxr2p57Yyd0Df87lGxopSAnNX7ysUseo3OqYUy7eXc,288730 +playwright/driver/package/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css,sha256=IL6CSoKDK2LMBGQtMdo8YJ_SgLI_HNyx5iLE9AqsWrg,6251 +playwright/driver/package/lib/vite/traceViewer/codicon.DCmgc-ay.ttf,sha256=Dx1SGZNOlug7jbFi1gtNjAm13h59OAMcuv5KPA8oick,80340 +playwright/driver/package/lib/vite/traceViewer/defaultSettingsView.C8BeHuxd.css,sha256=J4EBtoq0juYyOmr79ODmKTJOEOlpAqYqCHVkOhu6PZQ,107270 +playwright/driver/package/lib/vite/traceViewer/index.7-c5LSTW.js,sha256=zh65VrFXObUqRcn8K5oMUpRIJCQmsrEk1cMWeRWRD_E,6628 +playwright/driver/package/lib/vite/traceViewer/index.B_TqY17P.css,sha256=CKIS_KyuGGb0TwZWVK_MTH0DQJxox0szFcW9oXoMoAM,2055 +playwright/driver/package/lib/vite/traceViewer/index.html,sha256=uAZh1GvFgNHnvkXCwrDokcRm8_Z-UsM-TxEfAA81MsY,2363 +playwright/driver/package/lib/vite/traceViewer/manifest.webmanifest,sha256=2FQFAGA6MqOf5-WgN13A-euv0REY4GqEbbfvxggiOrk,429 +playwright/driver/package/lib/vite/traceViewer/playwright-logo.svg,sha256=awpDZ73qsQmVvCOSePBMaMEOSK2-wV55ngGQmg1m3Lk,5033 +playwright/driver/package/lib/vite/traceViewer/snapshot.B_Jk1wbt.js,sha256=1NCeBUUdEH2OGmu9BBQF_lGLn9KDOJDk66qtg7f1VPU,650 +playwright/driver/package/lib/vite/traceViewer/snapshot.html,sha256=xgjIiZyVxadNP1VuIBRsPYUrnfha4oVMZZp8x3PJtyc,389 +playwright/driver/package/lib/vite/traceViewer/sw.bundle.js,sha256=0db-xmh-s_2TqJkL2dqjlRQiaVr_-uI4L5fxCBHPHDA,96737 +playwright/driver/package/lib/vite/traceViewer/uiMode.CYwC_gH1.js,sha256=ySDqoWH2W2wN1DGmQsS98uYmKuUTNcAtFVsYf0jAKWw,38121 +playwright/driver/package/lib/vite/traceViewer/uiMode.CyMfwkXJ.css,sha256=-0A88LBwB7wddx4pc390CJJNowDi-riyiaPvLo_AjXs,54590 +playwright/driver/package/lib/vite/traceViewer/uiMode.html,sha256=beL12391MsMOixOBdSodryP6y972vqEwzvDPpaAD6yI,727 +playwright/driver/package/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css,sha256=DdYAVhNOaimUqGtZoel4m58E6Skk2hCa74ySOsrwjOU,2853 +playwright/driver/package/lib/webp_codec.LICENSE,sha256=mVx-41rOt2ZoKbelAcqJwIZOErSTwRdLFVd0qx4D2DE,8841 +playwright/driver/package/lib/webp_codec.wasm,sha256=FkzZWDo-TQgfdn07Qf8KcwRD3HU1_MXK_4NWFGM4S3k,565369 +playwright/driver/package/lib/xdg-open,sha256=fXWdVPDUzhvus07mLFh5hbUEI3To3iN7DcuM1HqmmM8,32416 +playwright/driver/package/package.json,sha256=YWVMomqWBT3EC7vmkvEzBMm3hhsLzgr7Z49okgJUZcw,895 +playwright/driver/package/types/protocol.d.ts,sha256=zNci3qXsplXPERNI7CejijdtRnQHxMx2PTFRRVDQTV0,819286 +playwright/driver/package/types/structs.d.ts,sha256=ybkXm3KRyFYusPt4Ehndvc82FJ69e9NV5bSlU9a2C4E,2455 +playwright/driver/package/types/types.d.ts,sha256=xFjJ2iHd53Mt1ED08vhh7GbviYE5VaWRFX7DDkqQGG4,1150893 +playwright/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +playwright/sync_api/__init__.py,sha256=KgDWVqeaZx7XjuRNPi3gJEJ2b28v-xZDS9b-5ONMTws,8119 +playwright/sync_api/__pycache__/__init__.cpython-311.pyc,, +playwright/sync_api/__pycache__/_context_manager.cpython-311.pyc,, +playwright/sync_api/__pycache__/_generated.cpython-311.pyc,, +playwright/sync_api/_context_manager.py,sha256=CKEl75BqYm4ItmK7rWakYEaEYxLvKrWFSrprqIUKug4,4651 +playwright/sync_api/_generated.py,sha256=l0n-Ul9Q4lhdHTpZEYHEOT-bF5-dIoGM51Sj7HL_8D0,971452 diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/REQUESTED b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/WHEEL b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/WHEEL new file mode 100644 index 0000000..14a883f --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/entry_points.txt b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/entry_points.txt new file mode 100644 index 0000000..f90baf7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +playwright = playwright.__main__:main + +[pyinstaller40] +hook-dirs = playwright._impl.__pyinstaller:get_hook_dirs diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000..4ace03d --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/licenses/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Portions Copyright (c) Microsoft Corporation. + Portions Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/top_level.txt new file mode 100644 index 0000000..508a5f4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright-1.64.1.dist-info/top_level.txt @@ -0,0 +1 @@ +playwright diff --git a/venv/lib/python3.11/site-packages/playwright/__init__.py b/venv/lib/python3.11/site-packages/playwright/__init__.py new file mode 100644 index 0000000..16593a9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Python package `playwright` is a Python library to automate Chromium, +Firefox and WebKit with a single API. Playwright is built to enable cross-browser +web automation that is ever-green, capable, reliable and fast. +""" diff --git a/venv/lib/python3.11/site-packages/playwright/__main__.py b/venv/lib/python3.11/site-packages/playwright/__main__.py new file mode 100644 index 0000000..b38ae8a --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/__main__.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import subprocess +import sys + +from playwright._impl._driver import compute_driver_executable, get_driver_env + + +def main() -> None: + try: + driver_executable, driver_cli = compute_driver_executable() + completed_process = subprocess.run( + [driver_executable, driver_cli, *sys.argv[1:]], env=get_driver_env() + ) + sys.exit(completed_process.returncode) + except KeyboardInterrupt: + sys.exit(130) + + +if __name__ == "__main__": + main() diff --git a/venv/lib/python3.11/site-packages/playwright/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a4b96ad Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/__pycache__/__main__.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..800a346 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/__pycache__/__main__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/__pycache__/_repo_version.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/__pycache__/_repo_version.cpython-311.pyc new file mode 100644 index 0000000..c4b1265 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/__pycache__/_repo_version.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__init__.py b/venv/lib/python3.11/site-packages/playwright/_impl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..aca03e3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_api_structures.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_api_structures.cpython-311.pyc new file mode 100644 index 0000000..1e9c055 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_api_structures.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_artifact.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_artifact.cpython-311.pyc new file mode 100644 index 0000000..acd53ec Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_artifact.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_assertions.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_assertions.cpython-311.pyc new file mode 100644 index 0000000..e427441 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_assertions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_async_base.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_async_base.cpython-311.pyc new file mode 100644 index 0000000..77b1566 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_async_base.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser.cpython-311.pyc new file mode 100644 index 0000000..9859b16 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser_context.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser_context.cpython-311.pyc new file mode 100644 index 0000000..0c7c630 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser_context.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser_type.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser_type.cpython-311.pyc new file mode 100644 index 0000000..663eb0c Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_browser_type.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_cdp_session.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_cdp_session.cpython-311.pyc new file mode 100644 index 0000000..b31ea84 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_cdp_session.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_clock.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_clock.cpython-311.pyc new file mode 100644 index 0000000..367db1b Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_clock.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_connection.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_connection.cpython-311.pyc new file mode 100644 index 0000000..58a943a Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_connection.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_console_message.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_console_message.cpython-311.pyc new file mode 100644 index 0000000..fe3001b Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_console_message.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_credentials.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_credentials.cpython-311.pyc new file mode 100644 index 0000000..7d7f758 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_credentials.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_debugger.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_debugger.cpython-311.pyc new file mode 100644 index 0000000..4f608a5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_debugger.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_dialog.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_dialog.cpython-311.pyc new file mode 100644 index 0000000..d765416 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_dialog.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_disposable.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_disposable.cpython-311.pyc new file mode 100644 index 0000000..878229f Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_disposable.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_download.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_download.cpython-311.pyc new file mode 100644 index 0000000..908a1f0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_download.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_driver.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_driver.cpython-311.pyc new file mode 100644 index 0000000..d042222 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_driver.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_element_handle.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_element_handle.cpython-311.pyc new file mode 100644 index 0000000..1cb28ac Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_element_handle.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_errors.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_errors.cpython-311.pyc new file mode 100644 index 0000000..c524ca3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_errors.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_event_context_manager.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_event_context_manager.cpython-311.pyc new file mode 100644 index 0000000..584ddc7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_event_context_manager.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_fetch.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_fetch.cpython-311.pyc new file mode 100644 index 0000000..07708da Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_fetch.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_file_chooser.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_file_chooser.cpython-311.pyc new file mode 100644 index 0000000..2d52067 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_file_chooser.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_form_data.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_form_data.cpython-311.pyc new file mode 100644 index 0000000..3354a71 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_form_data.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_frame.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_frame.cpython-311.pyc new file mode 100644 index 0000000..3cd4d8e Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_frame.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_glob.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_glob.cpython-311.pyc new file mode 100644 index 0000000..084f57d Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_glob.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_greenlets.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_greenlets.cpython-311.pyc new file mode 100644 index 0000000..b6c46a6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_greenlets.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_har_router.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_har_router.cpython-311.pyc new file mode 100644 index 0000000..c57e662 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_har_router.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_helper.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_helper.cpython-311.pyc new file mode 100644 index 0000000..cbb77e0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_helper.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_impl_to_api_mapping.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_impl_to_api_mapping.cpython-311.pyc new file mode 100644 index 0000000..aa0105e Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_impl_to_api_mapping.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_input.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_input.cpython-311.pyc new file mode 100644 index 0000000..0152ac9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_input.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_internal_structures.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_internal_structures.cpython-311.pyc new file mode 100644 index 0000000..c8631eb Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_internal_structures.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_js_handle.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_js_handle.cpython-311.pyc new file mode 100644 index 0000000..ada6379 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_js_handle.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_json_pipe.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_json_pipe.cpython-311.pyc new file mode 100644 index 0000000..5f3567d Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_json_pipe.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_local_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_local_utils.cpython-311.pyc new file mode 100644 index 0000000..21a9184 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_local_utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_locator.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_locator.cpython-311.pyc new file mode 100644 index 0000000..54097b9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_locator.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_map.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_map.cpython-311.pyc new file mode 100644 index 0000000..4d1aec7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_map.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_network.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_network.cpython-311.pyc new file mode 100644 index 0000000..737c47e Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_network.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_object_factory.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_object_factory.cpython-311.pyc new file mode 100644 index 0000000..2d3c779 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_object_factory.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_page.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_page.cpython-311.pyc new file mode 100644 index 0000000..322813e Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_page.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_path_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_path_utils.cpython-311.pyc new file mode 100644 index 0000000..78002e7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_path_utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_playwright.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_playwright.cpython-311.pyc new file mode 100644 index 0000000..d930161 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_playwright.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_screencast.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_screencast.cpython-311.pyc new file mode 100644 index 0000000..b639037 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_screencast.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_selectors.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_selectors.cpython-311.pyc new file mode 100644 index 0000000..1f1a1c0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_selectors.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_set_input_files_helpers.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_set_input_files_helpers.cpython-311.pyc new file mode 100644 index 0000000..b93eac5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_set_input_files_helpers.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_signature.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_signature.cpython-311.pyc new file mode 100644 index 0000000..c9ed8ce Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_signature.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_str_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_str_utils.cpython-311.pyc new file mode 100644 index 0000000..5532645 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_str_utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_stream.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_stream.cpython-311.pyc new file mode 100644 index 0000000..4a845ca Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_stream.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_sync_base.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_sync_base.cpython-311.pyc new file mode 100644 index 0000000..2982773 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_sync_base.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_tracing.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_tracing.cpython-311.pyc new file mode 100644 index 0000000..014c144 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_tracing.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_transport.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_transport.cpython-311.pyc new file mode 100644 index 0000000..f10cf2a Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_transport.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_video.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_video.cpython-311.pyc new file mode 100644 index 0000000..f5c7c1f Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_video.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_waiter.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_waiter.cpython-311.pyc new file mode 100644 index 0000000..d7143eb Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_waiter.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_web_error.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_web_error.cpython-311.pyc new file mode 100644 index 0000000..324f5cb Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_web_error.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_web_storage.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_web_storage.cpython-311.pyc new file mode 100644 index 0000000..e2b2a69 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_web_storage.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_webmcp.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_webmcp.cpython-311.pyc new file mode 100644 index 0000000..245ad6d Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_webmcp.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_writable_stream.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_writable_stream.cpython-311.pyc new file mode 100644 index 0000000..15c18a7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pycache__/_writable_stream.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__init__.py b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__init__.py new file mode 100644 index 0000000..a6f9feb --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import List + + +def get_hook_dirs() -> List[str]: + return [os.path.dirname(__file__)] diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7d480d5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/hook-playwright.async_api.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/hook-playwright.async_api.cpython-311.pyc new file mode 100644 index 0000000..e909eb7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/hook-playwright.async_api.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/hook-playwright.sync_api.cpython-311.pyc b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/hook-playwright.sync_api.cpython-311.pyc new file mode 100644 index 0000000..3c961d3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/__pycache__/hook-playwright.sync_api.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/hook-playwright.async_api.py b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/hook-playwright.async_api.py new file mode 100644 index 0000000..0300f0c --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/hook-playwright.async_api.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from PyInstaller.utils.hooks import collect_data_files # type: ignore + +datas = collect_data_files("playwright") diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/hook-playwright.sync_api.py b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/hook-playwright.sync_api.py new file mode 100644 index 0000000..0300f0c --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/__pyinstaller/hook-playwright.sync_api.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from PyInstaller.utils.hooks import collect_data_files # type: ignore + +datas = collect_data_files("playwright") diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_api_structures.py b/venv/lib/python3.11/site-packages/playwright/_impl/_api_structures.py new file mode 100644 index 0000000..5fed451 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_api_structures.py @@ -0,0 +1,249 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Generated from the docs by scripts/codegen/generate_api.js. Do not edit. + +from pathlib import Path +from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict, Union + + +class BrowserBindResult(TypedDict): + endpoint: str + + +class ClientCertificate(TypedDict, total=False): + origin: str + certPath: Optional[Union[str, Path]] + cert: Optional[bytes] + keyPath: Optional[Union[str, Path]] + key: Optional[bytes] + pfxPath: Optional[Union[str, Path]] + pfx: Optional[bytes] + passphrase: Optional[str] + + +class Cookie(TypedDict, total=False): + name: str + value: str + domain: str + path: str + expires: float + httpOnly: bool + secure: bool + sameSite: Literal["Lax", "None", "Strict"] + partitionKey: Optional[str] + + +class DebuggerLocation(TypedDict): + file: str + line: Optional[int] + column: Optional[int] + + +class DebuggerPausedDetails(TypedDict): + location: DebuggerLocation + title: str + + +class FilePayload(TypedDict): + name: str + mimeType: str + buffer: bytes + + +class DropPayload(TypedDict, total=False): + files: Optional[ + Union[str, Path, Sequence[Union[str, Path]], FilePayload, Sequence[FilePayload]] + ] + data: Optional[Dict[str, str]] + + +class FloatRect(TypedDict): + x: float + y: float + width: float + height: float + + +class Geolocation(TypedDict, total=False): + latitude: float + longitude: float + accuracy: Optional[float] + + +class HttpCredentials(TypedDict, total=False): + username: str + password: str + origin: Optional[str] + send: Optional[Literal["always", "unauthorized"]] + + +class LocalStorageEntry(TypedDict): + name: str + value: str + + +class NameValue(TypedDict): + name: str + value: str + + +class OriginState(TypedDict): + origin: str + localStorage: List[LocalStorageEntry] + + +class PdfMargins(TypedDict, total=False): + top: Optional[Union[str, float]] + right: Optional[Union[str, float]] + bottom: Optional[Union[str, float]] + left: Optional[Union[str, float]] + + +class Position(TypedDict): + x: float + y: float + + +class ProxySettings(TypedDict, total=False): + server: str + bypass: Optional[str] + username: Optional[str] + password: Optional[str] + + +class RemoteAddr(TypedDict): + ipAddress: str + port: int + + +class RequestSizes(TypedDict): + requestBodySize: int + requestHeadersSize: int + responseBodySize: int + responseHeadersSize: int + + +class ResourceTiming(TypedDict): + startTime: float + domainLookupStart: float + domainLookupEnd: float + connectStart: float + secureConnectionStart: float + connectEnd: float + requestStart: float + responseStart: float + responseEnd: float + + +class ScreencastActionStyle(TypedDict, total=False): + point: Optional[str] + highlight: Optional[str] + title: Optional[str] + + +class ScreencastFrame(TypedDict): + data: bytes + timestamp: float + viewportWidth: int + viewportHeight: int + + +class ScreencastSize(TypedDict): + width: int + height: int + + +class SecurityDetails(TypedDict): + issuer: Optional[str] + protocol: Optional[str] + subjectName: Optional[str] + validFrom: Optional[float] + validTo: Optional[float] + + +class SetCookieParam(TypedDict, total=False): + name: str + value: str + url: Optional[str] + domain: Optional[str] + path: Optional[str] + expires: Optional[float] + httpOnly: Optional[bool] + secure: Optional[bool] + sameSite: Optional[Literal["Lax", "None", "Strict"]] + partitionKey: Optional[str] + + +class SourceLocation(TypedDict): + url: str + line: int + column: int + lineNumber: int + columnNumber: int + + +class StorageStateCookie(TypedDict, total=False): + name: str + value: str + domain: str + path: str + expires: float + httpOnly: bool + secure: bool + sameSite: Literal["Lax", "None", "Strict"] + + +class StorageState(TypedDict, total=False): + cookies: List[StorageStateCookie] + origins: List[OriginState] + + +class TracingGroupLocation(TypedDict): + file: str + line: Optional[int] + column: Optional[int] + + +class ViewportSize(TypedDict): + width: int + height: int + + +class VirtualCredential(TypedDict): + id: str + rpId: str + userHandle: str + privateKey: str + publicKey: str + signCount: int + + +class WebErrorLocation(TypedDict): + url: str + line: int + column: int + + +class WebMCPToolAnnotations(TypedDict, total=False): + readOnly: Optional[bool] + untrustedContent: Optional[bool] + consequential: Optional[bool] + + +class WebMCPTool(TypedDict, total=False): + name: str + description: str + inputSchema: Optional[Any] + annotations: Optional[WebMCPToolAnnotations] diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_artifact.py b/venv/lib/python3.11/site-packages/playwright/_impl/_artifact.py new file mode 100644 index 0000000..a08294c --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_artifact.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +from pathlib import Path +from typing import Dict, Optional, Union, cast + +from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._helper import Error, make_dirs_for_file, patch_error_message +from playwright._impl._stream import Stream + + +class Artifact(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self.absolute_path = initializer["absolutePath"] + + async def path_after_finished(self) -> pathlib.Path: + if self._connection.is_remote: + raise Error( + "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." + ) + path = await self._channel.send( + "pathAfterFinished", + None, + ) + return pathlib.Path(path) + + async def save_as(self, path: Union[str, Path]) -> None: + stream = cast( + Stream, + from_channel( + await self._channel.send( + "saveAsStream", + None, + ) + ), + ) + make_dirs_for_file(path) + await stream.save_as(path) + + async def failure(self) -> Optional[str]: + reason = await self._channel.send( + "failure", + None, + ) + if reason is None: + return None + return patch_error_message(reason) + + async def delete(self) -> None: + await self._channel.send( + "delete", + None, + ) + + async def read_info_buffer(self) -> bytes: + stream = cast( + Stream, + from_channel( + await self._channel.send( + "stream", + None, + ) + ), + ) + buffer = await stream.read_all() + return buffer + + async def cancel(self) -> None: # pyright: ignore[reportIncompatibleMethodOverride] + await self._channel.send( + "cancel", + None, + ) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_assertions.py b/venv/lib/python3.11/site-packages/playwright/_impl/_assertions.py new file mode 100644 index 0000000..44c7c4a --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_assertions.py @@ -0,0 +1,1125 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections.abc +from contextlib import contextmanager +from typing import Any, Iterator, List, Literal, Optional, Pattern, Sequence, Union +from urllib.parse import urljoin + +from playwright._impl._connection import format_call_log +from playwright._impl._errors import Error +from playwright._impl._fetch import APIResponse +from playwright._impl._helper import is_textual_mime_type +from playwright._impl._internal_structures import ( + AriaRole, + ExpectedTextValue, + FrameExpectOptions, + FrameExpectResult, +) +from playwright._impl._js_handle import parse_value +from playwright._impl._locator import Locator +from playwright._impl._page import Page +from playwright._impl._str_utils import escape_regex_flags + +_soft_errors: Optional[List[AssertionError]] = None + + +@contextmanager +def _soft_scope() -> Iterator[List[AssertionError]]: + global _soft_errors + assert _soft_errors is None, "nested soft assertion scopes are not supported" + _soft_errors = [] + try: + yield _soft_errors + finally: + _soft_errors = None + + +def _record_soft_or_raise(error: AssertionError, is_soft: bool) -> None: + __tracebackhide__ = True + if is_soft: + if _soft_errors is None: + raise RuntimeError( + "expect.soft(...) requires pytest-playwright>=0.7.3 " + "(or pytest-playwright-asyncio>=0.7.3). Upgrade the plugin, " + "or use a regular expect(...) assertion." + ) + _soft_errors.append(error) + return + raise error + + +class AssertionsBase: + def __init__( + self, + locator: Locator, + timeout: float = None, + is_not: bool = False, + message: Optional[str] = None, + is_soft: bool = False, + ) -> None: + self._actual_locator = locator + self._loop = locator._loop + self._dispatcher_fiber = locator._dispatcher_fiber + self._timeout = timeout + self._is_not = is_not + self._custom_message = message + self._is_soft = is_soft + + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + raise NotImplementedError( + "_call_expect must be implemented in a derived class." + ) + + async def _expect_impl( + self, + expression: str, + expect_options: FrameExpectOptions, + expected: Any, + message: str, + title: str = None, + ) -> None: + __tracebackhide__ = True + expect_options["isNot"] = self._is_not + if expect_options.get("timeout") is None: + expect_options["timeout"] = self._timeout or 5_000 + if expect_options["isNot"]: + message = message.replace("expected to", "expected not to") + if title: + title = title.replace('"to_', '"not_to_') + if "useInnerText" in expect_options and expect_options["useInnerText"] is None: + del expect_options["useInnerText"] + result = await self._call_expect(expression, expect_options, title) + if result["matches"] == self._is_not: + received = result.get("received") or {} + aria_snapshot = None + if isinstance(received, dict): + aria_snapshot = received.get("ariaSnapshot") + value = received.get("value") + actual = parse_value(value) if value is not None else None + else: + actual = received + if self._custom_message: + out_message = self._custom_message + if expected is not None: + out_message += f"\nExpected value: '{expected or ''}'" + else: + out_message = ( + f"{message} '{expected}'" if expected is not None else f"{message}" + ) + error_message = result.get("errorMessage") + error_message = f"\n{error_message}" if error_message else "" + aria_snapshot_message = ( + f"\nAria snapshot:\n{aria_snapshot}" if aria_snapshot else "" + ) + _record_soft_or_raise( + AssertionError( + f"{out_message}\nActual value: {actual}{error_message} {format_call_log(result.get('log'))}{aria_snapshot_message}" + ), + self._is_soft, + ) + + +class PageAssertions(AssertionsBase): + def __init__( + self, + page: Page, + timeout: float = None, + is_not: bool = False, + message: Optional[str] = None, + is_soft: bool = False, + ) -> None: + super().__init__(page.locator(":root"), timeout, is_not, message, is_soft) + self._actual_page = page + + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + __tracebackhide__ = True + return await self._actual_page.main_frame._expect( + None, expression, expect_options, title + ) + + @property + def _not(self) -> "PageAssertions": + return PageAssertions( + self._actual_page, + self._timeout, + not self._is_not, + self._custom_message, + self._is_soft, + ) + + async def to_have_title( + self, titleOrRegExp: Union[Pattern[str], str], timeout: float = None + ) -> None: + __tracebackhide__ = True + expected_values = to_expected_text_values( + [titleOrRegExp], normalize_white_space=True + ) + await self._expect_impl( + "to.have.title", + FrameExpectOptions(expectedText=expected_values, timeout=timeout), + titleOrRegExp, + "Page title expected to be", + 'Expect "to_have_title"', + ) + + async def not_to_have_title( + self, titleOrRegExp: Union[Pattern[str], str], timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._not.to_have_title(titleOrRegExp, timeout) + + async def to_have_url( + self, + urlOrRegExp: Union[str, Pattern[str]], + timeout: float = None, + ignoreCase: bool = None, + ) -> None: + __tracebackhide__ = True + base_url = self._actual_page.context._base_url + if isinstance(urlOrRegExp, str) and base_url: + urlOrRegExp = urljoin(base_url, urlOrRegExp) + expected_text = to_expected_text_values([urlOrRegExp], ignoreCase=ignoreCase) + await self._expect_impl( + "to.have.url", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + urlOrRegExp, + "Page URL expected to be", + 'Expect "to_have_url"', + ) + + async def not_to_have_url( + self, + urlOrRegExp: Union[Pattern[str], str], + timeout: float = None, + ignoreCase: bool = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_url(urlOrRegExp, timeout, ignoreCase) + + async def to_match_aria_snapshot( + self, expected: str, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.match.aria", + FrameExpectOptions(expectedValue=expected, timeout=timeout), + expected, + "Page expected to match Aria snapshot", + 'Expect "to_match_aria_snapshot"', + ) + + async def not_to_match_aria_snapshot( + self, expected: str, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._not.to_match_aria_snapshot(expected, timeout) + + +class LocatorAssertions(AssertionsBase): + def __init__( + self, + locator: Locator, + timeout: float = None, + is_not: bool = False, + message: Optional[str] = None, + is_soft: bool = False, + ) -> None: + super().__init__(locator, timeout, is_not, message, is_soft) + self._actual_locator = locator + + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + __tracebackhide__ = True + return await self._actual_locator._expect(expression, expect_options, title) + + @property + def _not(self) -> "LocatorAssertions": + return LocatorAssertions( + self._actual_locator, + self._timeout, + not self._is_not, + self._custom_message, + self._is_soft, + ) + + async def to_contain_text( + self, + expected: Union[ + Sequence[str], + Sequence[Pattern[str]], + Sequence[Union[Pattern[str], str]], + Pattern[str], + str, + ], + useInnerText: bool = None, + timeout: float = None, + ignoreCase: bool = None, + ) -> None: + __tracebackhide__ = True + if isinstance(expected, collections.abc.Sequence) and not isinstance( + expected, str + ): + expected_text = to_expected_text_values( + expected, + match_substring=True, + normalize_white_space=True, + ignoreCase=ignoreCase, + ) + await self._expect_impl( + "to.contain.text.array", + FrameExpectOptions( + expectedText=expected_text, + useInnerText=useInnerText, + timeout=timeout, + ), + expected, + "Locator expected to contain text", + 'Expect "to_contain_text"', + ) + else: + expected_text = to_expected_text_values( + [expected], + match_substring=True, + normalize_white_space=True, + ignoreCase=ignoreCase, + ) + await self._expect_impl( + "to.have.text", + FrameExpectOptions( + expectedText=expected_text, + useInnerText=useInnerText, + timeout=timeout, + ), + expected, + "Locator expected to contain text", + 'Expect "to_contain_text"', + ) + + async def not_to_contain_text( + self, + expected: Union[ + Sequence[str], + Sequence[Pattern[str]], + Sequence[Union[Pattern[str], str]], + Pattern[str], + str, + ], + useInnerText: bool = None, + timeout: float = None, + ignoreCase: bool = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_contain_text(expected, useInnerText, timeout, ignoreCase) + + async def to_have_attribute( + self, + name: str, + value: Union[Pattern[str], str] = None, + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if value is None: + await self._expect_impl( + "to.have.attribute", + FrameExpectOptions(expressionArg=name, timeout=timeout), + None, + "Locator expected to have attribute", + 'Expect "to_have_attribute"', + ) + return + expected_text = to_expected_text_values([value], ignoreCase=ignoreCase) + await self._expect_impl( + "to.have.attribute.value", + FrameExpectOptions( + expressionArg=name, expectedText=expected_text, timeout=timeout + ), + value, + "Locator expected to have attribute", + 'Expect "to_have_attribute"', + ) + + async def not_to_have_attribute( + self, + name: str, + value: Union[Pattern[str], str] = None, + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_attribute( + name, value, ignoreCase=ignoreCase, timeout=timeout + ) + + async def to_have_class( + self, + expected: Union[ + Sequence[str], + Sequence[Pattern[str]], + Sequence[Union[Pattern[str], str]], + Pattern[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if isinstance(expected, collections.abc.Sequence) and not isinstance( + expected, str + ): + expected_text = to_expected_text_values(expected) + await self._expect_impl( + "to.have.class.array", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to have class", + 'Expect "to_have_class"', + ) + else: + expected_text = to_expected_text_values([expected]) + await self._expect_impl( + "to.have.class", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to have class", + 'Expect "to_have_class"', + ) + + async def not_to_have_class( + self, + expected: Union[ + Sequence[str], + Sequence[Pattern[str]], + Sequence[Union[Pattern[str], str]], + Pattern[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_class(expected, timeout) + + async def to_contain_class( + self, + expected: Union[ + Sequence[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if isinstance(expected, collections.abc.Sequence) and not isinstance( + expected, str + ): + expected_text = to_expected_text_values(expected) + await self._expect_impl( + "to.contain.class.array", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to contain class names", + 'Expect "to_contain_class"', + ) + else: + expected_text = to_expected_text_values([expected]) + await self._expect_impl( + "to.contain.class", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to contain class", + 'Expect "to_contain_class"', + ) + + async def not_to_contain_class( + self, + expected: Union[ + Sequence[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_contain_class(expected, timeout) + + async def to_have_count( + self, + count: int, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.have.count", + FrameExpectOptions(expectedNumber=count, timeout=timeout), + count, + "Locator expected to have count", + 'Expect "to_have_count"', + ) + + async def not_to_have_count( + self, + count: int, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_count(count, timeout) + + async def to_have_css( + self, + name: str, + value: Union[str, Pattern[str]], + timeout: float = None, + pseudo: Literal["after", "before"] = None, + ) -> None: + __tracebackhide__ = True + expected_text = to_expected_text_values([value]) + await self._expect_impl( + "to.have.css", + FrameExpectOptions( + expressionArg=name, + expectedText=expected_text, + timeout=timeout, + pseudo=pseudo, + ), + value, + "Locator expected to have CSS", + 'Expect "to_have_css"', + ) + + async def not_to_have_css( + self, + name: str, + value: Union[str, Pattern[str]], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_css(name, value, timeout) + + async def to_have_id( + self, + id: Union[str, Pattern[str]], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + expected_text = to_expected_text_values([id]) + await self._expect_impl( + "to.have.id", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + id, + "Locator expected to have ID", + 'Expect "to_have_id"', + ) + + async def not_to_have_id( + self, + id: Union[str, Pattern[str]], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_id(id, timeout) + + async def to_have_js_property( + self, + name: str, + value: Any, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.have.property", + FrameExpectOptions( + expressionArg=name, expectedValue=value, timeout=timeout + ), + value, + "Locator expected to have JS Property", + 'Expect "to_have_property"', + ) + + async def not_to_have_js_property( + self, + name: str, + value: Any, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_js_property(name, value, timeout) + + async def to_have_value( + self, + value: Union[str, Pattern[str]], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + expected_text = to_expected_text_values([value]) + await self._expect_impl( + "to.have.value", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + value, + "Locator expected to have Value", + 'Expect "to_have_value"', + ) + + async def not_to_have_value( + self, + value: Union[str, Pattern[str]], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_value(value, timeout) + + async def to_have_values( + self, + values: Union[ + Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]] + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + expected_text = to_expected_text_values(values) + await self._expect_impl( + "to.have.values", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + values, + "Locator expected to have Values", + 'Expect "to_have_values"', + ) + + async def not_to_have_values( + self, + values: Union[ + Sequence[str], Sequence[Pattern[str]], Sequence[Union[Pattern[str], str]] + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_values(values, timeout) + + async def to_have_text( + self, + expected: Union[ + Sequence[str], + Sequence[Pattern[str]], + Sequence[Union[Pattern[str], str]], + Pattern[str], + str, + ], + useInnerText: bool = None, + timeout: float = None, + ignoreCase: bool = None, + ) -> None: + __tracebackhide__ = True + if isinstance(expected, collections.abc.Sequence) and not isinstance( + expected, str + ): + expected_text = to_expected_text_values( + expected, + normalize_white_space=True, + ignoreCase=ignoreCase, + ) + await self._expect_impl( + "to.have.text.array", + FrameExpectOptions( + expectedText=expected_text, + useInnerText=useInnerText, + timeout=timeout, + ), + expected, + "Locator expected to have text", + 'Expect "to_have_text"', + ) + else: + expected_text = to_expected_text_values( + [expected], normalize_white_space=True, ignoreCase=ignoreCase + ) + await self._expect_impl( + "to.have.text", + FrameExpectOptions( + expectedText=expected_text, + useInnerText=useInnerText, + timeout=timeout, + ), + expected, + "Locator expected to have text", + 'Expect "to_have_text"', + ) + + async def not_to_have_text( + self, + expected: Union[ + Sequence[str], + Sequence[Pattern[str]], + Sequence[Union[Pattern[str], str]], + Pattern[str], + str, + ], + useInnerText: bool = None, + timeout: float = None, + ignoreCase: bool = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_text(expected, useInnerText, timeout, ignoreCase) + + async def to_be_attached( + self, + attached: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if attached is None: + attached = True + attached_string = "attached" if attached else "detached" + await self._expect_impl( + ("to.be.attached" if attached else "to.be.detached"), + FrameExpectOptions(timeout=timeout), + None, + f"Locator expected to be {attached_string}", + 'Expect "to_be_attached"', + ) + + async def to_be_checked( + self, + timeout: float = None, + checked: bool = None, + indeterminate: bool = None, + ) -> None: + __tracebackhide__ = True + expected_value = {} + if indeterminate is not None: + expected_value["indeterminate"] = indeterminate + if checked is not None: + expected_value["checked"] = checked + checked_string: str + if indeterminate: + checked_string = "indeterminate" + else: + checked_string = "unchecked" if checked is False else "checked" + await self._expect_impl( + "to.be.checked", + FrameExpectOptions(timeout=timeout, expectedValue=expected_value), + None, + f"Locator expected to be {checked_string}", + 'Expect "to_be_checked"', + ) + + async def not_to_be_attached( + self, + attached: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_attached(attached=attached, timeout=timeout) + + async def not_to_be_checked( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_checked(timeout) + + async def to_be_disabled( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.be.disabled", + FrameExpectOptions(timeout=timeout), + None, + "Locator expected to be disabled", + 'Expect "to_be_disabled"', + ) + + async def not_to_be_disabled( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_disabled(timeout) + + async def to_be_editable( + self, + editable: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if editable is None: + editable = True + editable_string = "editable" if editable else "readonly" + await self._expect_impl( + "to.be.editable" if editable else "to.be.readonly", + FrameExpectOptions(timeout=timeout), + None, + f"Locator expected to be {editable_string}", + 'Expect "to_be_editable"', + ) + + async def not_to_be_editable( + self, + editable: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_editable(editable, timeout) + + async def to_be_empty( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.be.empty", + FrameExpectOptions(timeout=timeout), + None, + "Locator expected to be empty", + 'Expect "to_be_empty"', + ) + + async def not_to_be_empty( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_empty(timeout) + + async def to_be_enabled( + self, + enabled: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if enabled is None: + enabled = True + enabled_string = "enabled" if enabled else "disabled" + await self._expect_impl( + "to.be.enabled" if enabled else "to.be.disabled", + FrameExpectOptions(timeout=timeout), + None, + f"Locator expected to be {enabled_string}", + 'Expect "to_be_enabled"', + ) + + async def not_to_be_enabled( + self, + enabled: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_enabled(enabled, timeout) + + async def to_be_hidden( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.be.hidden", + FrameExpectOptions(timeout=timeout), + None, + "Locator expected to be hidden", + 'Expect "to_be_hidden"', + ) + + async def not_to_be_hidden( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_hidden(timeout) + + async def to_be_visible( + self, + visible: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if visible is None: + visible = True + visible_string = "visible" if visible else "hidden" + await self._expect_impl( + "to.be.visible" if visible else "to.be.hidden", + FrameExpectOptions(timeout=timeout), + None, + f"Locator expected to be {visible_string}", + 'Expect "to_be_visible"', + ) + + async def not_to_be_visible( + self, + visible: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_visible(visible, timeout) + + async def to_be_focused( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.be.focused", + FrameExpectOptions(timeout=timeout), + None, + "Locator expected to be focused", + 'Expect "to_be_focused"', + ) + + async def not_to_be_focused( + self, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_be_focused(timeout) + + async def to_be_in_viewport( + self, + ratio: float = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.be.in.viewport", + FrameExpectOptions(timeout=timeout, expectedNumber=ratio), + None, + "Locator expected to be in viewport", + 'Expect "to_be_in_viewport"', + ) + + async def not_to_be_in_viewport( + self, ratio: float = None, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._not.to_be_in_viewport(ratio=ratio, timeout=timeout) + + async def to_have_accessible_description( + self, + description: Union[str, Pattern[str]], + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + expected_values = to_expected_text_values( + [description], ignoreCase=ignoreCase, normalize_white_space=True + ) + await self._expect_impl( + "to.have.accessible.description", + FrameExpectOptions(expectedText=expected_values, timeout=timeout), + None, + "Locator expected to have accessible description", + 'Expect "to_have_accessible_description"', + ) + + async def not_to_have_accessible_description( + self, + name: Union[str, Pattern[str]], + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_accessible_description(name, ignoreCase, timeout) + + async def to_have_accessible_name( + self, + name: Union[str, Pattern[str]], + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + expected_values = to_expected_text_values( + [name], ignoreCase=ignoreCase, normalize_white_space=True + ) + await self._expect_impl( + "to.have.accessible.name", + FrameExpectOptions(expectedText=expected_values, timeout=timeout), + None, + "Locator expected to have accessible name", + 'Expect "to_have_accessible_name"', + ) + + async def not_to_have_accessible_name( + self, + name: Union[str, Pattern[str]], + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_accessible_name(name, ignoreCase, timeout) + + async def to_have_role(self, role: AriaRole, timeout: float = None) -> None: + __tracebackhide__ = True + if isinstance(role, Pattern): + raise Error('"role" argument in to_have_role must be a string') + expected_values = to_expected_text_values([role]) + await self._expect_impl( + "to.have.role", + FrameExpectOptions(expectedText=expected_values, timeout=timeout), + None, + "Locator expected to have accessible role", + 'Expect "to_have_role"', + ) + + async def to_have_accessible_error_message( + self, + errorMessage: Union[str, Pattern[str]], + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + expected_values = to_expected_text_values( + [errorMessage], ignoreCase=ignoreCase, normalize_white_space=True + ) + await self._expect_impl( + "to.have.accessible.error.message", + FrameExpectOptions(expectedText=expected_values, timeout=timeout), + None, + "Locator expected to have accessible error message", + 'Expect "to_have_accessible_error_message"', + ) + + async def not_to_have_accessible_error_message( + self, + errorMessage: Union[str, Pattern[str]], + ignoreCase: bool = None, + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_accessible_error_message( + errorMessage=errorMessage, ignoreCase=ignoreCase, timeout=timeout + ) + + async def not_to_have_role(self, role: AriaRole, timeout: float = None) -> None: + __tracebackhide__ = True + await self._not.to_have_role(role, timeout) + + async def to_match_aria_snapshot( + self, expected: str, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.match.aria", + FrameExpectOptions(expectedValue=expected, timeout=timeout), + expected, + "Locator expected to match Aria snapshot", + 'Expect "to_match_aria_snapshot"', + ) + + async def not_to_match_aria_snapshot( + self, expected: str, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._not.to_match_aria_snapshot(expected, timeout) + + +class APIResponseAssertions: + def __init__( + self, + response: APIResponse, + timeout: float = None, + is_not: bool = False, + message: Optional[str] = None, + is_soft: bool = False, + ) -> None: + self._loop = response._loop + self._dispatcher_fiber = response._dispatcher_fiber + self._timeout = timeout + self._is_not = is_not + self._actual = response + self._custom_message = message + self._is_soft = is_soft + + @property + def _not(self) -> "APIResponseAssertions": + return APIResponseAssertions( + self._actual, + self._timeout, + not self._is_not, + self._custom_message, + self._is_soft, + ) + + async def to_be_ok( + self, + ) -> None: + __tracebackhide__ = True + if self._is_not is not self._actual.ok: + return + message = f"Response status expected to be within [200..299] range, was '{self._actual.status}'" + if self._is_not: + message = message.replace("expected to", "expected not to") + out_message = self._custom_message or message + out_message += format_call_log(await self._actual._fetch_log()) + + content_type = self._actual.headers.get("content-type") + is_text_encoding = content_type and is_textual_mime_type(content_type) + text = await self._actual.text() if is_text_encoding else None + if text is not None: + out_message += f"\n Response Text:\n{text[:1000]}" + + _record_soft_or_raise(AssertionError(out_message), self._is_soft) + + async def not_to_be_ok(self) -> None: + __tracebackhide__ = True + await self._not.to_be_ok() + + +def expected_regex( + pattern: Pattern[str], + match_substring: bool, + normalize_white_space: bool, + ignoreCase: Optional[bool] = None, +) -> ExpectedTextValue: + expected = ExpectedTextValue( + regexSource=pattern.pattern, + regexFlags=escape_regex_flags(pattern), + matchSubstring=match_substring, + normalizeWhiteSpace=normalize_white_space, + ignoreCase=ignoreCase, + ) + if expected["ignoreCase"] is None: + del expected["ignoreCase"] + return expected + + +def to_expected_text_values( + items: Union[ + Sequence[Pattern[str]], Sequence[str], Sequence[Union[str, Pattern[str]]] + ], + match_substring: bool = False, + normalize_white_space: bool = False, + ignoreCase: Optional[bool] = None, +) -> Sequence[ExpectedTextValue]: + out: List[ExpectedTextValue] = [] + if not isinstance(items, (list, tuple)): + raise Error("Expected a list or tuple of strings or patterns") + for item in items: + if isinstance(item, str): + o = ExpectedTextValue( + string=item, + matchSubstring=match_substring, + normalizeWhiteSpace=normalize_white_space, + ignoreCase=ignoreCase, + ) + if o["ignoreCase"] is None: + del o["ignoreCase"] + out.append(o) + elif isinstance(item, Pattern): + out.append( + expected_regex(item, match_substring, normalize_white_space, ignoreCase) + ) + else: + raise Error("value must be a string or regular expression") + return out diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_async_base.py b/venv/lib/python3.11/site-packages/playwright/_impl/_async_base.py new file mode 100644 index 0000000..ea90181 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_async_base.py @@ -0,0 +1,103 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from contextlib import AbstractAsyncContextManager +from types import TracebackType +from typing import Any, Callable, Generic, Optional, Type, TypeVar, Union + +from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper + +mapping = ImplToApiMapping() + + +T = TypeVar("T") +Self = TypeVar("Self", bound="AsyncContextManager") + + +class AsyncEventInfo(Generic[T]): + def __init__(self, future: "asyncio.Future[T]") -> None: + self._future = future + + @property + async def value(self) -> T: + return mapping.from_maybe_impl(await self._future) + + def _cancel(self) -> None: + self._future.cancel() + + def is_done(self) -> bool: + return self._future.done() + + +class AsyncEventContextManager(Generic[T], AbstractAsyncContextManager): + def __init__(self, future: "asyncio.Future[T]") -> None: + self._event = AsyncEventInfo[T](future) + + async def __aenter__(self) -> AsyncEventInfo[T]: + return self._event + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + if exc_val: + self._event._cancel() + else: + await self._event.value + + +class AsyncBase(ImplWrapper): + def __init__(self, impl_obj: Any) -> None: + super().__init__(impl_obj) + self._loop = impl_obj._loop + + def __str__(self) -> str: + return self._impl_obj.__str__() + + def _wrap_handler(self, handler: Union[Callable[..., Any], Any]) -> Any: + if callable(handler): + return mapping.wrap_handler(handler) + return handler + + def on(self, event: Any, f: Any) -> None: + """Registers the function ``f`` to the event name ``event``.""" + self._impl_obj.on(event, self._wrap_handler(f)) + + def once(self, event: Any, f: Any) -> None: + """The same as ``self.on``, except that the listener is automatically + removed after being called. + """ + self._impl_obj.once(event, self._wrap_handler(f)) + + def remove_listener(self, event: Any, f: Any) -> None: + """Removes the function ``f`` from ``event``.""" + self._impl_obj.remove_listener(event, self._wrap_handler(f)) + + +class AsyncContextManager(AsyncBase): + async def __aenter__(self: Self) -> Self: + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await self.close() + + async def close(self) -> None: ... diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_browser.py b/venv/lib/python3.11/site-packages/playwright/_impl/_browser.py new file mode 100644 index 0000000..227d3cc --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_browser.py @@ -0,0 +1,296 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from types import SimpleNamespace +from typing import ( + TYPE_CHECKING, + Dict, + List, + Optional, + Pattern, + Sequence, + Set, + Union, + cast, +) + +from playwright._impl._api_structures import ( + BrowserBindResult, + ClientCertificate, + Geolocation, + HttpCredentials, + ProxySettings, + StorageState, + ViewportSize, +) +from playwright._impl._artifact import Artifact +from playwright._impl._browser_context import BrowserContext +from playwright._impl._cdp_session import CDPSession +from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._errors import is_target_closed_error +from playwright._impl._helper import ( + ColorScheme, + Contrast, + ForcedColors, + HarContentPolicy, + HarMode, + ReducedMotion, + ServiceWorkersPolicy, + locals_to_params, + make_dirs_for_file, +) +from playwright._impl._page import Page + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser_type import BrowserType + + +class Browser(ChannelOwner): + Events = SimpleNamespace( + Context="context", + Disconnected="disconnected", + ) + + def __init__( + self, parent: "BrowserType", type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._browser_type: Optional["BrowserType"] = None + self._is_connected = True + self._should_close_connection_on_close = False + self._cr_tracing_path: Optional[str] = None + + self._contexts: Set[BrowserContext] = set() + self._traces_dir: Optional[str] = None + self._channel.on( + "context", + lambda params: self._did_create_context( + cast(BrowserContext, from_channel(params["context"])) + ), + ) + self._channel.on("close", lambda _: self._on_close()) + self._close_reason: Optional[str] = None + + def __repr__(self) -> str: + return f"" + + def _connect_to_browser_type( + self, + browser_type: "BrowserType", + traces_dir: Optional[str] = None, + ) -> None: + # Note: when using connect(), `browserType` is different from `this.parent`. + # This is why browser type is not wired up in the constructor, and instead this separate method is called later on. + self._browser_type = browser_type + self._traces_dir = traces_dir + for context in self._contexts: + self._setup_browser_context(context) + + def _did_create_context(self, context: BrowserContext) -> None: + context._browser = self + self._contexts.add(context) + # Note: when connecting to a browser, initial contexts arrive before `_browserType` is set, + # and will be configured later in `ConnectToBrowserType`. + if self._browser_type: + self._setup_browser_context(context) + self.emit(Browser.Events.Context, context) + + def _setup_browser_context(self, context: BrowserContext) -> None: + context._tracing._traces_dir = self._traces_dir + context._request._tracing._traces_dir = self._traces_dir + assert self._browser_type is not None + self._browser_type._playwright.selectors._contexts_for_selectors.add(context) + + def _on_close(self) -> None: + self._is_connected = False + self.emit(Browser.Events.Disconnected, self) + + @property + def contexts(self) -> List[BrowserContext]: + return list(self._contexts) + + @property + def browser_type(self) -> "BrowserType": + assert self._browser_type is not None + return self._browser_type + + def is_connected(self) -> bool: + return self._is_connected + + async def new_context( + self, + viewport: ViewportSize = None, + screen: ViewportSize = None, + noViewport: bool = None, + ignoreHTTPSErrors: bool = None, + javaScriptEnabled: bool = None, + bypassCSP: bool = None, + userAgent: str = None, + locale: str = None, + timezoneId: str = None, + geolocation: Geolocation = None, + permissions: Sequence[str] = None, + extraHTTPHeaders: Dict[str, str] = None, + offline: bool = None, + httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None, + deviceScaleFactor: float = None, + isMobile: bool = None, + hasTouch: bool = None, + colorScheme: ColorScheme = None, + reducedMotion: ReducedMotion = None, + forcedColors: ForcedColors = None, + contrast: Contrast = None, + acceptDownloads: bool = None, + defaultBrowserType: str = None, + proxy: ProxySettings = None, + recordHarPath: Union[Path, str] = None, + recordHarOmitContent: bool = None, + recordVideoDir: Union[Path, str] = None, + recordVideoSize: ViewportSize = None, + storageState: Union[StorageState, str, Path] = None, + baseURL: str = None, + strictSelectors: bool = None, + serviceWorkers: ServiceWorkersPolicy = None, + recordHarUrlFilter: Union[Pattern[str], str] = None, + recordHarMode: HarMode = None, + recordHarContent: HarContentPolicy = None, + clientCertificates: List[ClientCertificate] = None, + recordVideoFps: int = None, + ) -> BrowserContext: + params = locals_to_params(locals()) + assert self._browser_type is not None + await self._browser_type._prepare_browser_context_params(params) + + channel = await self._channel.send("newContext", None, params) + context = cast(BrowserContext, from_channel(channel)) + await context._initialize_har_from_options( + record_har_content=recordHarContent, + record_har_mode=recordHarMode, + record_har_omit_content=recordHarOmitContent, + record_har_path=recordHarPath, + record_har_url_filter=recordHarUrlFilter, + ) + return context + + async def new_page( + self, + viewport: ViewportSize = None, + screen: ViewportSize = None, + noViewport: bool = None, + ignoreHTTPSErrors: bool = None, + javaScriptEnabled: bool = None, + bypassCSP: bool = None, + userAgent: str = None, + locale: str = None, + timezoneId: str = None, + geolocation: Geolocation = None, + permissions: Sequence[str] = None, + extraHTTPHeaders: Dict[str, str] = None, + offline: bool = None, + httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None, + deviceScaleFactor: float = None, + isMobile: bool = None, + hasTouch: bool = None, + colorScheme: ColorScheme = None, + forcedColors: ForcedColors = None, + contrast: Contrast = None, + reducedMotion: ReducedMotion = None, + acceptDownloads: bool = None, + defaultBrowserType: str = None, + proxy: ProxySettings = None, + recordHarPath: Union[Path, str] = None, + recordHarOmitContent: bool = None, + recordVideoDir: Union[Path, str] = None, + recordVideoSize: ViewportSize = None, + storageState: Union[StorageState, str, Path] = None, + baseURL: str = None, + strictSelectors: bool = None, + serviceWorkers: ServiceWorkersPolicy = None, + recordHarUrlFilter: Union[Pattern[str], str] = None, + recordHarMode: HarMode = None, + recordHarContent: HarContentPolicy = None, + clientCertificates: List[ClientCertificate] = None, + recordVideoFps: int = None, + ) -> Page: + params = locals_to_params(locals()) + + async def inner() -> Page: + context = await self.new_context(**params) + page = await context.new_page() + page._owned_context = context + context._owner_page = page + return page + + return await self._connection.wrap_api_call(inner, title="Create page") + + async def close(self, reason: str = None) -> None: + self._close_reason = reason + try: + if self._should_close_connection_on_close: + await self._connection.stop_async() + else: + await self._channel.send("close", None, {"reason": reason}) + except Exception as e: + if not is_target_closed_error(e): + raise e + + @property + def version(self) -> str: + return self._initializer["version"] + + async def new_browser_cdp_session(self) -> CDPSession: + return from_channel(await self._channel.send("newBrowserCDPSession", None)) + + async def bind( + self, + title: str, + workspaceDir: str = None, + host: str = None, + port: int = None, + ) -> BrowserBindResult: + return await self._channel.send_return_as_dict( + "startServer", None, locals_to_params(locals()) + ) + + async def unbind(self) -> None: + await self._channel.send("stopServer", None) + + async def start_tracing( + self, + page: Page = None, + path: Union[str, Path] = None, + screenshots: bool = None, + categories: Sequence[str] = None, + ) -> None: + params = locals_to_params(locals()) + if page: + params["page"] = page._channel + if path: + self._cr_tracing_path = str(path) + params["path"] = str(path) + await self._channel.send("startTracing", None, params) + + async def stop_tracing(self) -> bytes: + artifact = cast( + Artifact, from_channel(await self._channel.send("stopTracing", None)) + ) + buffer = await artifact.read_info_buffer() + await artifact.delete() + if self._cr_tracing_path: + make_dirs_for_file(self._cr_tracing_path) + with open(self._cr_tracing_path, "wb") as f: + f.write(buffer) + self._cr_tracing_path = None + return buffer diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_browser_context.py b/venv/lib/python3.11/site-packages/playwright/_impl/_browser_context.py new file mode 100644 index 0000000..dc6c21d --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_browser_context.py @@ -0,0 +1,750 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import json +from pathlib import Path +from types import SimpleNamespace +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Literal, + Optional, + Pattern, + Sequence, + Set, + Union, + cast, +) + +from playwright._impl._api_structures import ( + Cookie, + Geolocation, + SetCookieParam, + StorageState, + WebErrorLocation, +) +from playwright._impl._cdp_session import CDPSession +from playwright._impl._clock import Clock +from playwright._impl._connection import ( + ChannelOwner, + from_channel, + from_nullable_channel, +) +from playwright._impl._console_message import ConsoleMessage +from playwright._impl._credentials import Credentials +from playwright._impl._debugger import Debugger +from playwright._impl._dialog import Dialog +from playwright._impl._disposable import Disposable, DisposableStub +from playwright._impl._errors import Error, TargetClosedError +from playwright._impl._event_context_manager import EventContextManagerImpl +from playwright._impl._fetch import APIRequestContext +from playwright._impl._frame import Frame +from playwright._impl._har_router import HarRouter +from playwright._impl._helper import ( + HarContentPolicy, + HarMode, + RouteFromHarNotFoundPolicy, + RouteHandler, + RouteHandlerCallback, + TimeoutSettings, + URLMatch, + WebSocketRouteHandlerCallback, + async_readfile, + async_writefile, + create_task_and_ignore_exception, + locals_to_params, + parse_error, + to_impl, +) +from playwright._impl._network import ( + Request, + Response, + Route, + WebSocketRoute, + WebSocketRouteHandler, + serialize_headers, + to_clear_cookies_params, +) +from playwright._impl._page import BindingCall, Page, Worker +from playwright._impl._tracing import Tracing +from playwright._impl._waiter import Waiter +from playwright._impl._web_error import WebError + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser import Browser + + +class BrowserContext(ChannelOwner): + Events = SimpleNamespace( + # Deprecated in v1.56, never emitted anymore. + BackgroundPage="backgroundpage", + Close="close", + Console="console", + Dialog="dialog", + DialogClosed="dialogclosed", + Download="download", + FrameAttached="frameattached", + FrameDetached="framedetached", + FrameNavigated="framenavigated", + Page="page", + PageClose="pageclose", + PageLoad="pageload", + WebError="weberror", + ServiceWorker="serviceworker", + Request="request", + Response="response", + RequestFailed="requestfailed", + RequestFinished="requestfinished", + ) + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + # Browser is null for browser contexts created outside of normal browser, e.g. android or electron. + # circular import workaround: + self._browser: Optional["Browser"] = None + if parent.__class__.__name__ == "Browser": + self._browser = cast("Browser", parent) + self._pages: List[Page] = [] + self._routes: List[RouteHandler] = [] + self._web_socket_routes: List[WebSocketRouteHandler] = [] + self._bindings: Dict[str, Any] = {} + self._timeout_settings = TimeoutSettings(None) + self._owner_page: Optional[Page] = None + self._options: Dict[str, Any] = initializer["options"] + self._service_workers: Set[Worker] = set() + self._base_url: Optional[str] = self._options.get("baseURL") + self._videos_dir: Optional[str] = self._options.get("recordVideo") + self._tracing = cast(Tracing, from_channel(initializer["tracing"])) + self._debugger: Debugger = cast(Debugger, from_channel(initializer["debugger"])) + self._request: APIRequestContext = from_channel(initializer["requestContext"]) + self._request._timeout_settings = self._timeout_settings + self._clock = Clock(self) + self._credentials = Credentials(self) + self._channel.on( + "bindingCall", + lambda params: self._on_binding(from_channel(params["binding"])), + ) + self._channel.on("close", lambda _: self._on_close()) + self._channel.on( + "page", lambda params: self._on_page(from_channel(params["page"])) + ) + self._channel.on( + "route", + lambda params: self._loop.create_task( + self._on_route( + from_channel(params.get("route")), + ) + ), + ) + self._channel.on( + "webSocketRoute", + lambda params: self._loop.create_task( + self._on_web_socket_route( + from_channel(params["webSocketRoute"]), + ) + ), + ) + + self._channel.on( + "serviceWorker", + lambda params: self._on_service_worker(from_channel(params["worker"])), + ) + self._channel.on( + "console", + lambda event: self._on_console_message(event), + ) + + self._channel.on( + "dialog", lambda params: self._on_dialog(from_channel(params["dialog"])) + ) + self._channel.on( + "dialogClosed", + lambda params: self._on_dialog_closed(from_channel(params["dialog"])), + ) + self._channel.on( + "pageError", + lambda params: self._on_page_error( + parse_error(params["error"]["error"]), + from_nullable_channel(params["page"]), + cast( + WebErrorLocation, + params.get("location") or {"url": "", "line": 0, "column": 0}, + ), + ), + ) + self._channel.on( + "request", + lambda params: self._on_request( + from_channel(params["request"]), + from_nullable_channel(params.get("page")), + ), + ) + self._channel.on( + "response", + lambda params: self._on_response( + from_channel(params["response"]), + from_nullable_channel(params.get("page")), + ), + ) + self._channel.on( + "requestFailed", + lambda params: self._on_request_failed( + from_channel(params["request"]), + params["responseEndTiming"], + params.get("failureText"), + from_nullable_channel(params.get("page")), + ), + ) + self._channel.on( + "requestFinished", + lambda params: self._on_request_finished( + from_channel(params["request"]), + from_nullable_channel(params.get("response")), + params["responseEndTiming"], + from_nullable_channel(params.get("page")), + ), + ) + self._closed_future: asyncio.Future = asyncio.Future() + self.once( + self.Events.Close, lambda context: self._closed_future.set_result(True) + ) + self._close_reason: Optional[str] = None + self._har_routers: List[HarRouter] = [] + self._set_event_to_subscription_mapping( + { + BrowserContext.Events.Console: "console", + BrowserContext.Events.Dialog: "dialog", + BrowserContext.Events.DialogClosed: "dialogClosed", + BrowserContext.Events.Request: "request", + BrowserContext.Events.Response: "response", + BrowserContext.Events.RequestFinished: "requestFinished", + BrowserContext.Events.RequestFailed: "requestFailed", + } + ) + self._closing_or_closed = False + + def __repr__(self) -> str: + return f"" + + def _on_page(self, page: Page) -> None: + self._pages.append(page) + self.emit(BrowserContext.Events.Page, page) + if page._opener and not page._opener.is_closed(): + page._opener.emit(Page.Events.Popup, page) + + async def _on_route(self, route: Route) -> None: + route._context = self + page = route.request._safe_page() + route_handlers = self._routes.copy() + for route_handler in route_handlers: + # If the page or the context was closed we stall all requests right away. + if (page and page._close_was_called) or self._closing_or_closed: + return + if not route_handler.matches(route.request.url): + continue + if route_handler not in self._routes: + continue + if route_handler.will_expire: + self._routes.remove(route_handler) + try: + handled = await route_handler.handle(route) + finally: + if len(self._routes) == 0: + create_task_and_ignore_exception( + self._loop, + self._connection.wrap_api_call( + lambda: self._update_interception_patterns(), True + ), + ) + if handled: + return + try: + # If the page is closed or unrouteAll() was called without waiting and interception disabled, + # the method will throw an error - silence it. + await route._inner_continue(True) + except Exception: + pass + + async def _on_web_socket_route(self, web_socket_route: WebSocketRoute) -> None: + route_handler = next( + ( + route_handler + for route_handler in self._web_socket_routes + if route_handler.matches(web_socket_route.url) + ), + None, + ) + if route_handler: + await route_handler.handle(web_socket_route) + else: + web_socket_route.connect_to_server() + + def _on_binding(self, binding_call: BindingCall) -> None: + func = self._bindings.get(binding_call._initializer["name"]) + if func is None: + return + asyncio.create_task(binding_call.call(func)) + + def set_default_navigation_timeout(self, timeout: float) -> None: + return self._set_default_navigation_timeout_impl(timeout) + + def _set_default_navigation_timeout_impl(self, timeout: Optional[float]) -> None: + self._timeout_settings.set_default_navigation_timeout(timeout) + + def set_default_timeout(self, timeout: float) -> None: + return self._set_default_timeout_impl(timeout) + + def _set_default_timeout_impl(self, timeout: Optional[float]) -> None: + self._timeout_settings.set_default_timeout(timeout) + + @property + def pages(self) -> List[Page]: + return self._pages.copy() + + @property + def browser(self) -> Optional["Browser"]: + return self._browser + + async def _initialize_har_from_options( + self, + record_har_path: Optional[Union[Path, str]], + record_har_content: Optional[HarContentPolicy], + record_har_omit_content: Optional[bool], + record_har_url_filter: Optional[Union[Pattern[str], str]], + record_har_mode: Optional[HarMode], + ) -> None: + if not record_har_path: + return + record_har_path = str(record_har_path) + default_policy: HarContentPolicy = ( + "attach" if record_har_path.endswith(".zip") else "embed" + ) + content_policy: HarContentPolicy = record_har_content or ( + "omit" if record_har_omit_content is True else default_policy + ) + await self._tracing._record_into_har( + har=record_har_path, + page=None, + url=record_har_url_filter, + update_content=content_policy, + update_mode=(record_har_mode or "full"), + ) + + async def new_page(self) -> Page: + if self._owner_page: + raise Error("Please use browser.new_context()") + return from_channel(await self._channel.send("newPage", None)) + + async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: + if urls is None: + urls = [] + if isinstance(urls, str): + urls = [urls] + return await self._channel.send("cookies", None, dict(urls=urls)) + + async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None: + await self._channel.send("addCookies", None, dict(cookies=cookies)) + + async def clear_cookies( + self, + name: Union[str, Pattern[str]] = None, + domain: Union[str, Pattern[str]] = None, + path: Union[str, Pattern[str]] = None, + ) -> None: + await self._channel.send( + "clearCookies", None, to_clear_cookies_params(name, domain, path) + ) + + async def grant_permissions( + self, permissions: Sequence[str], origin: str = None + ) -> None: + await self._channel.send("grantPermissions", None, locals_to_params(locals())) + + async def clear_permissions(self) -> None: + await self._channel.send("clearPermissions", None) + + async def set_geolocation(self, geolocation: Geolocation = None) -> None: + await self._channel.send("setGeolocation", None, locals_to_params(locals())) + + async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: + await self._channel.send( + "setExtraHTTPHeaders", None, dict(headers=serialize_headers(headers)) + ) + + async def set_offline(self, offline: bool) -> None: + await self._channel.send("setOffline", None, dict(offline=offline)) + + async def add_init_script( + self, script: str = None, path: Union[str, Path] = None + ) -> Disposable: + if path: + script = (await async_readfile(path)).decode() + if not isinstance(script, str): + raise Error("Either path or script parameter must be specified") + return from_channel( + await self._channel.send("addInitScript", None, dict(source=script)) + ) + + async def expose_binding(self, name: str, callback: Callable) -> Disposable: + for page in self._pages: + if name in page._bindings: + raise Error( + f'Function "{name}" has been already registered in one of the pages' + ) + if name in self._bindings: + raise Error(f'Function "{name}" has been already registered') + self._bindings[name] = callback + return from_channel( + await self._channel.send("exposeBinding", None, dict(name=name)) + ) + + async def expose_function(self, name: str, callback: Callable) -> Disposable: + return await self.expose_binding(name, lambda source, *args: callback(*args)) + + async def route( + self, url: URLMatch, handler: RouteHandlerCallback, times: int = None + ) -> DisposableStub: + self._routes.insert( + 0, + RouteHandler( + self._base_url, + url, + handler, + True if self._dispatcher_fiber else False, + times, + ), + ) + await self._update_interception_patterns() + return DisposableStub(lambda: self.unroute(url, handler), self) + + async def unroute( + self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None + ) -> None: + removed = [] + remaining = [] + for route in self._routes: + if route.url != url or (handler and route.handler != handler): + remaining.append(route) + else: + removed.append(route) + await self._unroute_internal(removed, remaining, "default") + + async def _unroute_internal( + self, + removed: List[RouteHandler], + remaining: List[RouteHandler], + behavior: Literal["default", "ignoreErrors", "wait"] = None, + ) -> None: + self._routes = remaining + if behavior is not None and behavior != "default": + await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore + await self._update_interception_patterns() + + async def route_web_socket( + self, url: URLMatch, handler: WebSocketRouteHandlerCallback + ) -> None: + self._web_socket_routes.insert( + 0, + WebSocketRouteHandler(self._base_url, url, handler), + ) + await self._update_web_socket_interception_patterns() + + def _dispose_har_routers(self) -> None: + for router in self._har_routers: + router.dispose() + self._har_routers = [] + + async def unroute_all( + self, behavior: Literal["default", "ignoreErrors", "wait"] = None + ) -> None: + await self._unroute_internal(self._routes, [], behavior) + self._dispose_har_routers() + + async def route_from_har( + self, + har: Union[Path, str], + url: Union[Pattern[str], str] = None, + notFound: RouteFromHarNotFoundPolicy = None, + update: bool = None, + updateContent: Literal["attach", "embed"] = None, + updateMode: HarMode = None, + ) -> None: + if update: + await self._tracing._record_into_har( + har=har, + page=None, + url=url, + update_content=updateContent, + update_mode=updateMode, + ) + return + router = await HarRouter.create( + local_utils=self._connection.local_utils, + file=str(har), + not_found_action=notFound or "abort", + url_matcher=url, + ) + self._har_routers.append(router) + await router.add_context_route(self) + + async def _update_interception_patterns(self) -> None: + patterns = RouteHandler.prepare_interception_patterns(self._routes) + await self._channel.send( + "setNetworkInterceptionPatterns", None, {"patterns": patterns} + ) + + async def _update_web_socket_interception_patterns(self) -> None: + patterns = WebSocketRouteHandler.prepare_interception_patterns( + self._web_socket_routes + ) + await self._channel.send( + "setWebSocketInterceptionPatterns", None, {"patterns": patterns} + ) + + def expect_event( + self, + event: str, + predicate: Callable = None, + timeout: float = None, + ) -> EventContextManagerImpl: + if timeout is None: + timeout = self._timeout_settings.timeout() + waiter = Waiter(self, f"browser_context.expect_event({event})") + waiter.reject_on_timeout( + timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"' + ) + if event != BrowserContext.Events.Close: + waiter.reject_on_event( + self, BrowserContext.Events.Close, lambda: TargetClosedError() + ) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) + + def _on_close(self) -> None: + self._closing_or_closed = True + if self._browser: + if self in self._browser._contexts: + self._browser._contexts.remove(self) + assert self._browser._browser_type is not None + if ( + self + in self._browser._browser_type._playwright.selectors._contexts_for_selectors + ): + self._browser._browser_type._playwright.selectors._contexts_for_selectors.remove( + self + ) + + self._dispose_har_routers() + self._tracing._reset_stack_counter() + self._request._tracing._reset_stack_counter() + self.emit(BrowserContext.Events.Close, self) + + def is_closed(self) -> bool: + return self._closing_or_closed + + async def close(self, reason: str = None) -> None: + if self._closing_or_closed: + return + self._close_reason = reason + self._closing_or_closed = True + + await self.request.dispose(reason=reason) + + async def _inner_close() -> None: + await self._tracing._export_all_hars() + + await self._channel._connection.wrap_api_call(_inner_close, True) + await self._channel.send("close", None, {"reason": reason}) + await self._closed_future + + async def storage_state( + self, + path: Union[str, Path] = None, + indexedDB: bool = None, + opfs: bool = None, + credentials: bool = None, + ) -> StorageState: + result = await self._channel.send_return_as_dict( + "storageState", + None, + {"indexedDB": indexedDB, "opfs": opfs, "credentials": credentials}, + ) + if path: + await async_writefile(path, json.dumps(result)) + return result + + async def set_storage_state( + self, storageState: Union[StorageState, str, Path] + ) -> None: + if isinstance(storageState, (str, Path)): + state = json.loads(await async_readfile(storageState)) + else: + state = storageState + await self._channel.send("setStorageState", None, {"storageState": state}) + + def _effective_close_reason(self) -> Optional[str]: + if self._close_reason: + return self._close_reason + if self._browser: + return self._browser._close_reason + return None + + async def wait_for_event( + self, event: str, predicate: Callable = None, timeout: float = None + ) -> Any: + async with self.expect_event(event, predicate, timeout) as event_info: + pass + return await event_info + + def expect_console_message( + self, + predicate: Callable[[ConsoleMessage], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl[ConsoleMessage]: + return self.expect_event(Page.Events.Console, predicate, timeout) + + def expect_page( + self, + predicate: Callable[[Page], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl[Page]: + return self.expect_event(BrowserContext.Events.Page, predicate, timeout) + + def _on_service_worker(self, worker: Worker) -> None: + worker._context = self + self._service_workers.add(worker) + self.emit(BrowserContext.Events.ServiceWorker, worker) + + def _on_request_failed( + self, + request: Request, + response_end_timing: float, + failure_text: Optional[str], + page: Optional[Page], + ) -> None: + request._failure_text = failure_text + request._set_response_end_timing(response_end_timing) + self.emit(BrowserContext.Events.RequestFailed, request) + if page: + page.emit(Page.Events.RequestFailed, request) + + def _on_request_finished( + self, + request: Request, + response: Optional[Response], + response_end_timing: float, + page: Optional[Page], + ) -> None: + request._set_response_end_timing(response_end_timing) + self.emit(BrowserContext.Events.RequestFinished, request) + if page: + page.emit(Page.Events.RequestFinished, request) + if response: + response._finished_future.set_result(True) + + def _on_console_message(self, event: Dict) -> None: + message = ConsoleMessage(event, self._loop, self._dispatcher_fiber) + worker = message.worker + if worker: + worker.emit(Worker.Events.Console, message) + page = message.page + if page: + page.emit(Page.Events.Console, message) + self.emit(BrowserContext.Events.Console, message) + + def _on_dialog(self, dialog: Dialog) -> None: + has_listeners = self.emit(BrowserContext.Events.Dialog, dialog) + page = dialog.page + if page: + has_listeners = page.emit(Page.Events.Dialog, dialog) or has_listeners + if not has_listeners: + # Although we do similar handling on the server side, we still need this logic + # on the client side due to a possible race condition between two async calls: + # a) removing "dialog" listener subscription (client->server) + # b) actual "dialog" event (server->client) + if dialog.type == "beforeunload": + create_task_and_ignore_exception(self._loop, dialog.accept()) + else: + create_task_and_ignore_exception(self._loop, dialog.dismiss()) + + def _on_dialog_closed(self, dialog: Dialog) -> None: + self.emit(BrowserContext.Events.DialogClosed, dialog) + page = dialog.page + if page: + page.emit(Page.Events.DialogClosed, dialog) + + def _on_page_error( + self, error: Error, page: Optional[Page], location: WebErrorLocation + ) -> None: + self.emit( + BrowserContext.Events.WebError, + WebError(self._loop, self._dispatcher_fiber, page, error, location), + ) + if page: + page.emit(Page.Events.PageError, error) + + def _on_request(self, request: Request, page: Optional[Page]) -> None: + self.emit(BrowserContext.Events.Request, request) + if page: + page.emit(Page.Events.Request, request) + + def _on_response(self, response: Response, page: Optional[Page]) -> None: + self.emit(BrowserContext.Events.Response, response) + if page: + page.emit(Page.Events.Response, response) + + @property + def background_pages(self) -> List[Page]: + return [] + + @property + def service_workers(self) -> List[Worker]: + return list(self._service_workers) + + async def new_cdp_session(self, page: Union[Page, Frame]) -> CDPSession: + page = to_impl(page) + params = {} + if isinstance(page, Page): + params["page"] = page._channel + elif isinstance(page, Frame): + params["frame"] = page._channel + else: + raise Error("page: expected Page or Frame") + return from_channel(await self._channel.send("newCDPSession", None, params)) + + @property + def tracing(self) -> Tracing: + return self._tracing + + @property + def debugger(self) -> Debugger: + return self._debugger + + @property + def request(self) -> "APIRequestContext": + return self._request + + @property + def clock(self) -> Clock: + return self._clock + + @property + def credentials(self) -> Credentials: + return self._credentials diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_browser_type.py b/venv/lib/python3.11/site-packages/playwright/_impl/_browser_type.py new file mode 100644 index 0000000..a4632cc --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_browser_type.py @@ -0,0 +1,380 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import json +import pathlib +from pathlib import Path +from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast + +from playwright._impl._api_structures import ( + ClientCertificate, + Geolocation, + HttpCredentials, + ProxySettings, + ViewportSize, +) +from playwright._impl._browser import Browser +from playwright._impl._browser_context import BrowserContext +from playwright._impl._connection import ChannelOwner, Connection, from_channel +from playwright._impl._errors import Error +from playwright._impl._helper import ( + PLAYWRIGHT_MAX_DEADLINE, + ColorScheme, + Contrast, + Env, + ForcedColors, + HarContentPolicy, + HarMode, + ReducedMotion, + ServiceWorkersPolicy, + TimeoutSettings, + async_readfile, + locals_to_params, +) +from playwright._impl._json_pipe import JsonPipeTransport +from playwright._impl._network import ( + serialize_headers, + to_client_certificates_protocol, + to_http_credentials_protocol, +) +from playwright._impl._waiter import throw_on_timeout + +if TYPE_CHECKING: + from playwright._impl._playwright import Playwright + + +class BrowserType(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._playwright: "Playwright" + + def __repr__(self) -> str: + return f"" + + @property + def name(self) -> str: + return self._initializer["name"] + + @property + def executable_path(self) -> str: + return self._initializer["executablePath"] + + async def launch( + self, + executablePath: Union[str, Path] = None, + channel: str = None, + args: Sequence[str] = None, + ignoreDefaultArgs: Union[bool, Sequence[str]] = None, + handleSIGINT: bool = None, + handleSIGTERM: bool = None, + handleSIGHUP: bool = None, + timeout: float = None, + env: Env = None, + headless: bool = None, + proxy: ProxySettings = None, + downloadsPath: Union[str, Path] = None, + slowMo: float = None, + tracesDir: Union[pathlib.Path, str] = None, + artifactsDir: Union[pathlib.Path, str] = None, + chromiumSandbox: bool = None, + firefoxUserPrefs: Dict[str, Union[str, float, bool]] = None, + ) -> Browser: + params = locals_to_params(locals()) + normalize_launch_params(params) + browser = cast( + Browser, + from_channel( + await self._channel.send( + "launch", TimeoutSettings.launch_timeout, params + ) + ), + ) + browser._connect_to_browser_type( + self, str(tracesDir) if tracesDir is not None else None + ) + return browser + + async def launch_persistent_context( + self, + userDataDir: Union[str, Path], + channel: str = None, + executablePath: Union[str, Path] = None, + args: Sequence[str] = None, + ignoreDefaultArgs: Union[bool, Sequence[str]] = None, + handleSIGINT: bool = None, + handleSIGTERM: bool = None, + handleSIGHUP: bool = None, + timeout: float = None, + env: Env = None, + headless: bool = None, + proxy: ProxySettings = None, + downloadsPath: Union[str, Path] = None, + slowMo: float = None, + viewport: ViewportSize = None, + screen: ViewportSize = None, + noViewport: bool = None, + ignoreHTTPSErrors: bool = None, + javaScriptEnabled: bool = None, + bypassCSP: bool = None, + userAgent: str = None, + locale: str = None, + timezoneId: str = None, + geolocation: Geolocation = None, + permissions: Sequence[str] = None, + extraHTTPHeaders: Dict[str, str] = None, + offline: bool = None, + httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None, + deviceScaleFactor: float = None, + isMobile: bool = None, + hasTouch: bool = None, + colorScheme: ColorScheme = None, + reducedMotion: ReducedMotion = None, + forcedColors: ForcedColors = None, + contrast: Contrast = None, + acceptDownloads: bool = None, + tracesDir: Union[pathlib.Path, str] = None, + artifactsDir: Union[pathlib.Path, str] = None, + chromiumSandbox: bool = None, + firefoxUserPrefs: Dict[str, Union[str, float, bool]] = None, + recordHarPath: Union[Path, str] = None, + recordHarOmitContent: bool = None, + recordVideoDir: Union[Path, str] = None, + recordVideoSize: ViewportSize = None, + baseURL: str = None, + strictSelectors: bool = None, + serviceWorkers: ServiceWorkersPolicy = None, + recordHarUrlFilter: Union[Pattern[str], str] = None, + recordHarMode: HarMode = None, + recordHarContent: HarContentPolicy = None, + clientCertificates: List[ClientCertificate] = None, + recordVideoFps: int = None, + ) -> BrowserContext: + userDataDir = self._user_data_dir(userDataDir) + params = locals_to_params(locals()) + await self._prepare_browser_context_params(params) + normalize_launch_params(params) + result = await self._channel.send_return_as_dict( + "launchPersistentContext", TimeoutSettings.launch_timeout, params + ) + browser = cast( + Browser, + from_channel(result["browser"]), + ) + browser._connect_to_browser_type( + self, str(tracesDir) if tracesDir is not None else None + ) + context = cast(BrowserContext, from_channel(result["context"])) + await context._initialize_har_from_options( + record_har_content=recordHarContent, + record_har_mode=recordHarMode, + record_har_omit_content=recordHarOmitContent, + record_har_path=recordHarPath, + record_har_url_filter=recordHarUrlFilter, + ) + return context + + def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: + if not userDataDir: + return "" + if not Path(userDataDir).is_absolute(): + return str(Path(userDataDir).resolve()) + return str(Path(userDataDir)) + + async def connect_over_cdp( + self, + endpointURL: str, + timeout: float = None, + slowMo: float = None, + headers: Dict[str, str] = None, + isLocal: bool = None, + noDefaults: bool = None, + artifactsDir: Union[str, Path] = None, + ) -> Browser: + params = locals_to_params(locals()) + if params.get("headers"): + params["headers"] = serialize_headers(params["headers"]) + response = await self._channel.send_return_as_dict( + "connectOverCDP", TimeoutSettings.launch_timeout, params + ) + browser = cast(Browser, from_channel(response["browser"])) + browser._connect_to_browser_type(self, None) + + return browser + + async def connect( + self, + endpoint: str, + timeout: float = None, + slowMo: float = None, + headers: Dict[str, str] = None, + exposeNetwork: str = None, + ) -> Browser: + if slowMo is None: + slowMo = 0 + if timeout is None: + timeout = 0 + + headers = {**(headers if headers else {}), "x-playwright-browser": self.name} + local_utils = self._connection.local_utils + pipe_channel = ( + await local_utils._channel.send_return_as_dict( + "connect", + lambda t: t or 0, + { + "endpoint": endpoint, + "headers": headers, + "slowMo": slowMo, + "timeout": timeout, + "exposeNetwork": exposeNetwork, + }, + ) + )["pipe"] + transport = JsonPipeTransport(self._connection._loop, pipe_channel) + + connection = Connection( + self._connection._dispatcher_fiber, + self._connection._object_factory, + transport, + self._connection._loop, + local_utils=self._connection.local_utils, + ) + connection.mark_as_remote() + + browser = None + + def handle_transport_close(reason: Optional[str]) -> None: + if browser: + for context in browser.contexts: + for page in context.pages: + page._on_close() + context._on_close() + connection.cleanup(reason) + # Give a chance to any API call promises to reject upon page/context closure. + # This happens naturally when we receive page.onClose and browser.onClose from the server + # in separate tasks. However, upon pipe closure we used to dispatch them all synchronously + # here and promises did not have a chance to reject. + # The order of rejects vs closure is a part of the API contract and our test runner + # relies on it to attribute rejections to the right test. + if browser: + connection._loop.call_soon(browser._on_close) + + transport.once("close", handle_transport_close) + + connection._is_sync = self._connection._is_sync + connection._loop.create_task(connection.run()) + playwright_future = connection.playwright_future + + timeout_future = throw_on_timeout( + timeout if timeout else PLAYWRIGHT_MAX_DEADLINE, + Error("Connection timed out"), + ) + done, pending = await asyncio.wait( + {transport.on_error_future, playwright_future, timeout_future}, + return_when=asyncio.FIRST_COMPLETED, + ) + if not playwright_future.done(): + playwright_future.cancel() + if not timeout_future.done(): + timeout_future.cancel() + playwright: "Playwright" = next(iter(done)).result() + playwright._set_selectors(self._playwright.selectors) + self._connection._child_ws_connections.append(connection) + pre_launched_browser = playwright._initializer.get("preLaunchedBrowser") + assert pre_launched_browser + browser = cast(Browser, from_channel(pre_launched_browser)) + browser._should_close_connection_on_close = True + browser._connect_to_browser_type(self, None) + + return browser + + async def _prepare_browser_context_params(self, params: Dict) -> None: + if params.get("noViewport"): + del params["noViewport"] + params["noDefaultViewport"] = True + if "defaultBrowserType" in params: + del params["defaultBrowserType"] + if "extraHTTPHeaders" in params: + params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) + if "recordVideoDir" in params: + params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()} + if "recordVideoSize" in params: + params["recordVideo"]["size"] = params["recordVideoSize"] + del params["recordVideoSize"] + if "recordVideoFps" in params: + params["recordVideo"]["fps"] = params["recordVideoFps"] + del params["recordVideoDir"] + params.pop("recordVideoFps", None) + if "storageState" in params: + storageState = params["storageState"] + if not isinstance(storageState, dict): + params["storageState"] = json.loads( + (await async_readfile(storageState)).decode() + ) + if params.get("colorScheme", None) == "null": + params["colorScheme"] = "no-override" + if params.get("reducedMotion", None) == "null": + params["reducedMotion"] = "no-override" + if params.get("forcedColors", None) == "null": + params["forcedColors"] = "no-override" + if params.get("contrast", None) == "null": + params["contrast"] = "no-override" + if "acceptDownloads" in params: + params["acceptDownloads"] = ( + "accept" if params["acceptDownloads"] else "deny" + ) + + if "clientCertificates" in params: + params["clientCertificates"] = await to_client_certificates_protocol( + params["clientCertificates"] + ) + if "httpCredentials" in params: + params["httpCredentials"] = to_http_credentials_protocol( + params["httpCredentials"] + ) + params["selectorEngines"] = self._playwright.selectors._selector_engines + params["testIdAttributeName"] = ( + self._playwright.selectors._test_id_attribute_name + ) + + # Remove HAR options + params.pop("recordHarPath", None) + params.pop("recordHarOmitContent", None) + params.pop("recordHarUrlFilter", None) + params.pop("recordHarMode", None) + params.pop("recordHarContent", None) + + +def normalize_launch_params(params: Dict) -> None: + if "env" in params: + params["env"] = [ + {"name": name, "value": str(value)} + for [name, value] in params["env"].items() + ] + if "ignoreDefaultArgs" in params: + if params["ignoreDefaultArgs"] is True: + params["ignoreAllDefaultArgs"] = True + del params["ignoreDefaultArgs"] + elif params["ignoreDefaultArgs"] is False: + del params["ignoreDefaultArgs"] + if "executablePath" in params: + params["executablePath"] = str(Path(params["executablePath"])) + if "downloadsPath" in params: + params["downloadsPath"] = str(Path(params["downloadsPath"])) + if "tracesDir" in params: + params["tracesDir"] = str(Path(params["tracesDir"])) + if "artifactsDir" in params: + params["artifactsDir"] = str(Path(params["artifactsDir"])) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_cdp_session.py b/venv/lib/python3.11/site-packages/playwright/_impl/_cdp_session.py new file mode 100644 index 0000000..07d291a --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_cdp_session.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import SimpleNamespace +from typing import Any, Dict + +from playwright._impl._connection import ChannelOwner +from playwright._impl._helper import locals_to_params + + +class CDPSession(ChannelOwner): + Events = SimpleNamespace( + Event="event", + Close="close", + ) + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._channel.on("event", lambda params: self._on_event(params)) + self._channel.on("close", lambda _: self.emit(CDPSession.Events.Close, self)) + + def _on_event(self, params: Any) -> None: + self.emit(params["method"], params.get("params")) + self.emit(CDPSession.Events.Event, params) + + async def send(self, method: str, params: Dict = None) -> Dict: + return await self._channel.send("send", None, locals_to_params(locals())) + + async def detach(self) -> None: + await self._channel.send( + "detach", + None, + ) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_clock.py b/venv/lib/python3.11/site-packages/playwright/_impl/_clock.py new file mode 100644 index 0000000..f6eb7c4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_clock.py @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +from typing import TYPE_CHECKING, Dict, Union + +if TYPE_CHECKING: + from playwright._impl._browser_context import BrowserContext + + +class Clock: + def __init__(self, browser_context: "BrowserContext") -> None: + self._browser_context = browser_context + self._loop = browser_context._loop + self._dispatcher_fiber = browser_context._dispatcher_fiber + + async def install(self, time: Union[float, str, datetime.datetime] = None) -> None: + await self._browser_context._channel.send( + "clockInstall", + None, + parse_time(time) if time is not None else {}, + ) + + async def fast_forward( + self, + ticks: Union[int, str], + ) -> None: + await self._browser_context._channel.send( + "clockFastForward", + None, + parse_ticks(ticks), + ) + + async def pause_at( + self, + time: Union[float, str, datetime.datetime], + ) -> None: + await self._browser_context._channel.send( + "clockPauseAt", + None, + parse_time(time), + ) + + async def resume( + self, + ) -> None: + await self._browser_context._channel.send("clockResume", None) + + async def run_for( + self, + ticks: Union[int, str], + ) -> None: + await self._browser_context._channel.send( + "clockRunFor", + None, + parse_ticks(ticks), + ) + + async def set_fixed_time( + self, + time: Union[float, str, datetime.datetime], + ) -> None: + await self._browser_context._channel.send( + "clockSetFixedTime", + None, + parse_time(time), + ) + + async def set_system_time( + self, + time: Union[float, str, datetime.datetime], + ) -> None: + await self._browser_context._channel.send( + "clockSetSystemTime", + None, + parse_time(time), + ) + + +def parse_time( + time: Union[float, str, datetime.datetime], +) -> Dict[str, Union[int, str]]: + if isinstance(time, (float, int)): + return {"timeNumber": int(time * 1_000)} + if isinstance(time, str): + return {"timeString": time} + return {"timeNumber": int(time.timestamp() * 1_000)} + + +def parse_ticks(ticks: Union[int, str]) -> Dict[str, Union[int, str]]: + if isinstance(ticks, int): + return {"ticksNumber": ticks} + return {"ticksString": ticks} diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_connection.py b/venv/lib/python3.11/site-packages/playwright/_impl/_connection.py new file mode 100644 index 0000000..9480823 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_connection.py @@ -0,0 +1,768 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import collections.abc +import contextvars +import datetime +import inspect +import random +import string +import sys +import traceback +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Mapping, + Optional, + Tuple, + TypedDict, + Union, + cast, +) + +from pyee import EventEmitter +from pyee.asyncio import AsyncIOEventEmitter + +import playwright +import playwright._impl._impl_to_api_mapping +from playwright._impl._errors import TargetClosedError, rewrite_error +from playwright._impl._greenlets import EventGreenlet +from playwright._impl._helper import ( + Error, + ParsedMessagePayload, + create_task_and_ignore_exception, + parse_error, +) +from playwright._impl._transport import Transport + +if TYPE_CHECKING: + from playwright._impl._local_utils import LocalUtils + from playwright._impl._playwright import Playwright + +TimeoutCalculator = Optional[Callable[[Optional[float]], float]] +_PLAYWRIGHT_MODULE_PATH = str(Path(playwright.__file__).parents[0]) + + +class Channel(AsyncIOEventEmitter): + def __init__(self, connection: "Connection", object: "ChannelOwner") -> None: + super().__init__() + self._connection = connection + self._guid = object._guid + self._object = object + self.on("error", lambda exc: self._connection._on_event_listener_error(exc)) + + async def send( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> Any: + return await self._connection.wrap_api_call( + lambda: self._inner_send(method, timeout_calculator, params, False), + is_internal, + title, + ) + + def send_may_fail( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> None: + create_task_and_ignore_exception( + self._connection._loop, + self.send(method, timeout_calculator, params, is_internal, title), + ) + + async def send_return_as_dict( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> Any: + return await self._connection.wrap_api_call( + lambda: self._inner_send(method, timeout_calculator, params, True), + is_internal, + title, + ) + + def send_no_reply( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> None: + # No reply messages are used to e.g. __waitInfo__(after). + augmented_params, timeout = _augment_params(params, timeout_calculator) + self._connection.wrap_api_call_sync( + lambda: self._connection._send_message_to_server( + self._object, + method, + augmented_params, + timeout, + True, + ), + is_internal, + title, + ) + + async def _inner_send( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Optional[Dict], + return_as_dict: bool, + ) -> Any: + if self._connection._error: + error = self._connection._error + self._connection._error = None + raise error + augmented_params, timeout = _augment_params(params, timeout_calculator) + callback = self._connection._send_message_to_server( + self._object, method, augmented_params, timeout + ) + try: + done, _ = await asyncio.wait( + { + self._connection._transport.on_error_future, + callback.future, + }, + return_when=asyncio.FIRST_COMPLETED, + ) + except asyncio.CancelledError as exc: + await self._connection._abort( + self._object, + callback, + str(exc) or "Task was cancelled", + ) + raise + if not callback.future.done(): + callback.future.cancel() + result = next(iter(done)).result() + # Protocol now has named return values, assume result is one level deeper unless + # there is explicit ambiguity. + if not result: + return None + assert isinstance(result, dict) + if return_as_dict: + return result + if len(result) == 0: + return None + assert len(result) == 1 + key = next(iter(result)) + return result[key] + + +class ChannelOwner(AsyncIOEventEmitter): + def __init__( + self, + parent: Union["ChannelOwner", "Connection"], + type: str, + guid: str, + initializer: Dict, + ) -> None: + super().__init__(loop=parent._loop) + self._loop: asyncio.AbstractEventLoop = parent._loop + self._dispatcher_fiber: Any = parent._dispatcher_fiber + self._type = type + self._guid: str = guid + self._connection: Connection = ( + parent._connection if isinstance(parent, ChannelOwner) else parent + ) + self._parent: Optional[ChannelOwner] = ( + parent if isinstance(parent, ChannelOwner) else None + ) + self._objects: Dict[str, "ChannelOwner"] = {} + self._channel: Channel = Channel(self._connection, self) + self._initializer = initializer + self._was_collected = False + + self._connection._objects[guid] = self + if self._parent: + self._parent._objects[guid] = self + + self._event_to_subscription_mapping: Dict[str, str] = {} + + def _dispose(self, reason: Optional[str]) -> None: + # Clean up from parent and connection. + if self._parent: + del self._parent._objects[self._guid] + del self._connection._objects[self._guid] + self._was_collected = reason == "gc" + + # Dispose all children. + for object in list(self._objects.values()): + object._dispose(reason) + self._objects.clear() + + def _adopt(self, child: "ChannelOwner") -> None: + del cast("ChannelOwner", child._parent)._objects[child._guid] + self._objects[child._guid] = child + child._parent = self + + def _set_event_to_subscription_mapping(self, mapping: Dict[str, str]) -> None: + self._event_to_subscription_mapping = mapping + + def _update_subscription(self, event: str, enabled: bool) -> None: + protocol_event = self._event_to_subscription_mapping.get(event) + if protocol_event: + self._connection.wrap_api_call_sync( + lambda: self._channel.send_no_reply( + "updateSubscription", + None, + {"event": protocol_event, "enabled": enabled}, + ), + True, + ) + + def _add_event_handler(self, event: str, k: Any, v: Any) -> None: + if not self.listeners(event): + self._update_subscription(event, True) + super()._add_event_handler(event, k, v) + + def remove_listener(self, event: str, f: Any) -> None: + super().remove_listener(event, f) + if not self.listeners(event): + self._update_subscription(event, False) + + +class ProtocolCallback: + def __init__( + self, loop: asyncio.AbstractEventLoop, id: str, no_reply: bool = False + ) -> None: + self.id = id + self.stack_trace: traceback.StackSummary + self.no_reply = no_reply + self.future = loop.create_future() + if no_reply: + self.future.set_result(None) + # The outer task can get cancelled by the user, this forwards the cancellation to the inner task. + current_task = asyncio.current_task() + + def cb(task: asyncio.Task) -> None: + if current_task: + current_task.remove_done_callback(cb) + if task.cancelled(): + self.future.cancel() + + if current_task: + current_task.add_done_callback(cb) + self.future.add_done_callback( + lambda _: ( + current_task.remove_done_callback(cb) if current_task else None + ) + ) + + +class RootChannelOwner(ChannelOwner): + def __init__(self, connection: "Connection") -> None: + super().__init__(connection, "Root", "", {}) + + async def initialize(self) -> "Playwright": + return from_channel( + await self._channel.send( + "initialize", + None, + { + "sdkLanguage": "python", + }, + ) + ) + + +class Connection(EventEmitter): + def __init__( + self, + dispatcher_fiber: Any, + object_factory: Callable[[ChannelOwner, str, str, Dict], ChannelOwner], + transport: Transport, + loop: asyncio.AbstractEventLoop, + local_utils: Optional["LocalUtils"] = None, + ) -> None: + super().__init__() + self._dispatcher_fiber = dispatcher_fiber + self._transport = transport + self._transport.on_message = lambda msg: self.dispatch(msg) + # Call ids double as trace call ids, so they must be unique across connections. + self._call_id_prefix = "".join(random.choices(string.ascii_lowercase, k=4)) + self._last_id = 0 + self._objects: Dict[str, ChannelOwner] = {} + self._callbacks: Dict[str, ProtocolCallback] = {} + self._object_factory = object_factory + self._is_sync = False + self._child_ws_connections: List["Connection"] = [] + self._loop = loop + self.playwright_future: asyncio.Future["Playwright"] = loop.create_future() + self._error: Optional[BaseException] = None + self.is_remote = False + self._init_task: Optional[asyncio.Task] = None + self._api_zone: contextvars.ContextVar[Optional[ParsedStackTrace]] = ( + contextvars.ContextVar("ApiZone", default=None) + ) + self._local_utils: Optional["LocalUtils"] = local_utils + self._tracing_count = 0 + self._closed_error: Optional[Exception] = None + + @property + def local_utils(self) -> "LocalUtils": + assert self._local_utils + return self._local_utils + + def mark_as_remote(self) -> None: + self.is_remote = True + + async def run_as_sync(self) -> None: + self._is_sync = True + await self.run() + + async def run(self) -> None: + self._loop = asyncio.get_running_loop() + self._root_object = RootChannelOwner(self) + + async def init() -> None: + try: + result = await self._root_object.initialize() + if not self.playwright_future.done(): + self.playwright_future.set_result(result) + except Exception as exc: + # No re-raise: callers observe playwright_future; a task + # exception would log "never retrieved". Skip set_* if async + # __aenter__ already cancelled the future after a transport error. + if not self.playwright_future.done(): + self.playwright_future.set_exception(exc) + + await self._transport.connect() + self._init_task = self._loop.create_task(init()) + await self._transport.run() + + def stop_sync(self) -> None: + self._transport.request_stop() + self._dispatcher_fiber.switch() + self._loop.run_until_complete(self._transport.wait_until_stopped()) + self.cleanup() + + async def stop_async(self) -> None: + self._transport.request_stop() + await self._transport.wait_until_stopped() + self.cleanup() + + def cleanup(self, cause: str = None) -> None: + self._closed_error = TargetClosedError(cause) if cause else TargetClosedError() + if self._init_task and not self._init_task.done(): + self._init_task.cancel() + for ws_connection in self._child_ws_connections: + ws_connection._transport.dispose() + for callback in self._callbacks.values(): + # To prevent 'Future exception was never retrieved' we ignore all callbacks that are no_reply. + if callback.no_reply: + continue + if callback.future.cancelled(): + continue + callback.future.set_exception(self._closed_error) + self._callbacks.clear() + self.emit("close") + + def set_is_tracing(self, is_tracing: bool) -> None: + if is_tracing: + self._tracing_count += 1 + else: + self._tracing_count -= 1 + + def _send_message_to_server( + self, + object: ChannelOwner, + method: str, + params: Dict, + timeout: float, + no_reply: bool = False, + ) -> ProtocolCallback: + if self._closed_error: + raise self._closed_error + if object._was_collected: + raise Error( + "The object has been collected to prevent unbounded heap growth." + ) + self._last_id += 1 + id = f"{self._call_id_prefix}@{self._last_id}" + callback = ProtocolCallback(self._loop, id, no_reply=no_reply) + task = asyncio.current_task(self._loop) + callback.stack_trace = cast( + traceback.StackSummary, + getattr(task, "__pw_stack_trace__", None) + or traceback.extract_stack(limit=10), + ) + callback.no_reply = no_reply + stack_trace_information = cast(ParsedStackTrace, self._api_zone.get()) + frames = stack_trace_information.get("frames", []) + location = ( + { + "file": frames[0]["file"], + "line": frames[0]["line"], + "column": frames[0]["column"], + } + if frames + else None + ) + metadata = { + "wallTime": int(datetime.datetime.now().timestamp() * 1000), + "apiName": stack_trace_information["apiName"], + "internal": not stack_trace_information["apiName"], + "timeout": timeout, + } + if location: + metadata["location"] = location # type: ignore + title = stack_trace_information["title"] + if title: + metadata["title"] = title + message = { + "id": id, + "guid": object._guid, + "method": method, + "params": self._replace_channels_with_guids(params), + "metadata": metadata, + } + if self._tracing_count > 0 and frames and object._guid != "localUtils": + self.local_utils.add_stack_to_tracing_no_reply(id, frames) + + self._transport.send(message) + self._callbacks[id] = callback + + return callback + + async def _abort( + self, object: ChannelOwner, callback: ProtocolCallback, reason: str + ) -> None: + try: + self._transport.send( + { + "guid": object._guid, + "method": "__abort__", + "params": {"id": callback.id, "reason": reason}, + } + ) + except (Error, OSError): + pass + try: + done, _ = await asyncio.wait( + { + self._transport.on_error_future, + callback.future, + }, + return_when=asyncio.FIRST_COMPLETED, + ) + finally: + if not callback.future.done(): + callback.future.cancel() + for future in done: + if not future.cancelled(): + future.exception() + + def dispatch(self, msg: ParsedMessagePayload) -> None: + if self._closed_error: + return + id = msg.get("id") + if id: + callback = self._callbacks.pop(id) + if callback.future.cancelled(): + return + # No reply messages are used to e.g. __waitInfo__(after) which returns exceptions on page close. + # To prevent 'Future exception was never retrieved' we just ignore such messages. + if callback.no_reply: + return + error = msg.get("error") + if error and not msg.get("result"): + parsed_error = parse_error( + error["error"], format_call_log(msg.get("log")) # type: ignore + ) + parsed_error._log = msg.get("log") + parsed_error._details = self._replace_guids_with_channels( + msg.get("errorDetails") + ) + parsed_error._stack = "".join(callback.stack_trace.format()) + callback.future.set_exception(parsed_error) + else: + result = self._replace_guids_with_channels(msg.get("result")) + callback.future.set_result(result) + return + + guid = msg["guid"] + method = msg["method"] + params = msg.get("params") + if method == "__create__": + assert params + parent = self._objects[guid] + self._create_remote_object( + parent, params["type"], params["guid"], params["initializer"] + ) + return + + object = self._objects.get(guid) + if not object: + raise Exception(f'Cannot find object to "{method}": {guid}') + + if method == "__adopt__": + child_guid = cast(Dict[str, str], params)["guid"] + child = self._objects.get(child_guid) + if not child: + raise Exception(f"Unknown new child: {child_guid}") + object._adopt(child) + return + + if method == "__dispose__": + assert isinstance(params, dict) + self._objects[guid]._dispose(cast(Optional[str], params.get("reason"))) + return + object = self._objects[guid] + should_replace_guids_with_channels = "jsonPipe@" not in guid + try: + if self._is_sync: + for listener in object._channel.listeners(method): + # Event handlers like route/locatorHandlerTriggered require us to perform async work. + # In order to report their potential errors to the user, we need to catch it and store it in the connection + def _done_callback(future: asyncio.Future) -> None: + exc = future.exception() + if exc: + self._on_event_listener_error(exc) + + def _listener_with_error_handler_attached(params: Any) -> None: + potential_future = listener(params) + if asyncio.isfuture(potential_future): + potential_future.add_done_callback(_done_callback) + + # Each event handler is a potentilly blocking context, create a fiber for each + # and switch to them in order, until they block inside and pass control to each + # other and then eventually back to dispatcher as listener functions return. + g = EventGreenlet(_listener_with_error_handler_attached) + if should_replace_guids_with_channels: + g.switch(self._replace_guids_with_channels(params)) + else: + g.switch(params) + else: + if should_replace_guids_with_channels: + object._channel.emit( + method, self._replace_guids_with_channels(params) + ) + else: + object._channel.emit(method, params) + except BaseException as exc: + self._on_event_listener_error(exc) + + def _on_event_listener_error(self, exc: BaseException) -> None: + print("Error occurred in event listener", file=sys.stderr) + traceback.print_exception(type(exc), exc, exc.__traceback__, file=sys.stderr) + # Save the error to throw at the next API call. This "replicates" unhandled rejection in Node.js. + self._error = exc + + def _create_remote_object( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> ChannelOwner: + initializer = self._replace_guids_with_channels(initializer) + return self._object_factory(parent, type, guid, initializer) + + def _replace_channels_with_guids( + self, + payload: Any, + ) -> Any: + if payload is None: + return payload + if isinstance(payload, Path): + return str(payload) + if isinstance(payload, collections.abc.Sequence) and not isinstance( + payload, str + ): + return list(map(self._replace_channels_with_guids, payload)) + if isinstance(payload, Channel): + return dict(guid=payload._guid) + if isinstance(payload, dict): + result = {} + for key, value in payload.items(): + result[key] = self._replace_channels_with_guids(value) + return result + return payload + + def _replace_guids_with_channels(self, payload: Any) -> Any: + if payload is None: + return payload + if isinstance(payload, list): + return list(map(self._replace_guids_with_channels, payload)) + if isinstance(payload, dict): + if payload.get("guid") in self._objects: + return self._objects[payload["guid"]]._channel + result = {} + for key, value in payload.items(): + result[key] = self._replace_guids_with_channels(value) + return result + return payload + + async def wrap_api_call( + self, cb: Callable[[], Any], is_internal: bool = False, title: str = None + ) -> Any: + if self._api_zone.get(): + return await cb() + task = asyncio.current_task(self._loop) + parsed_st = _attach_api_call_information( + getattr(task, "__pw_stack__", None) or _capture_stack_trace(), + is_internal, + title, + ) + self._api_zone.set(parsed_st) + try: + return await cb() + except Exception as error: + raise rewrite_error(error, f"{parsed_st['apiName']}: {error}") from None + finally: + self._api_zone.set(None) + + def wrap_api_call_sync( + self, cb: Callable[[], Any], is_internal: bool = False, title: str = None + ) -> Any: + if self._api_zone.get(): + return cb() + task = asyncio.current_task(self._loop) + parsed_st = _attach_api_call_information( + getattr(task, "__pw_stack__", None) or _capture_stack_trace(), + is_internal, + title, + ) + self._api_zone.set(parsed_st) + try: + return cb() + except Exception as error: + raise rewrite_error(error, f"{parsed_st['apiName']}: {error}") from None + finally: + self._api_zone.set(None) + + +def from_channel(channel: Channel) -> Any: + return channel._object + + +def from_nullable_channel(channel: Optional[Channel]) -> Optional[Any]: + return channel._object if channel else None + + +class StackFrame(TypedDict): + file: str + line: int + column: int + function: Optional[str] + + +class ParsedStackTrace(TypedDict): + frames: List[StackFrame] + apiName: Optional[str] + title: Optional[str] + + +def _capture_stack_trace() -> ParsedStackTrace: + frame = inspect.currentframe() + # Skip this helper and the caller that only captures the stack. + frame = frame.f_back.f_back if frame and frame.f_back else None + last_internal_api_name = "" + api_name = "" + parsed_frames: List[StackFrame] = [] + while frame: + code = frame.f_code + filename = code.co_filename + # Sync and Async implementations can have event handlers. When these are sync, they + # get evaluated in the context of the event loop, so they contain the stack trace of when + # the message was received. _impl_to_api_mapping is glue between the user-code and internal + # code to translate impl classes to api classes. We want to ignore these frames. + if playwright._impl._impl_to_api_mapping.__file__ == filename: + frame = frame.f_back + continue + is_playwright_internal = filename.startswith(_PLAYWRIGHT_MODULE_PATH) + + method_name = "" + if "self" in frame.f_locals: + method_name = frame.f_locals["self"].__class__.__name__ + "." + method_name += code.co_name + + if not is_playwright_internal: + parsed_frames.append( + { + "file": filename, + "line": frame.f_lineno, + "column": 0, + "function": method_name, + } + ) + if is_playwright_internal: + last_internal_api_name = method_name + elif last_internal_api_name: + api_name = last_internal_api_name + last_internal_api_name = "" + frame = frame.f_back + if not api_name: + api_name = last_internal_api_name + + return { + "frames": parsed_frames, + "apiName": api_name, + "title": None, + } + + +def _attach_api_call_information( + stack_trace_information: ParsedStackTrace, + is_internal: bool, + title: Optional[str], +) -> ParsedStackTrace: + if is_internal: + stack_trace_information["apiName"] = "" + stack_trace_information["title"] = title + return stack_trace_information + + +def _augment_params( + params: Optional[Dict], + timeout_calculator: Optional[Callable[[Optional[float]], float]], +) -> Tuple[Dict, float]: + if params is None: + params = {} + timeout_param = params.pop("timeout", None) + timeout: float = 0 + if timeout_calculator: + timeout = timeout_calculator(timeout_param) + return _filter_none(params), timeout + + +def _filter_none(d: Mapping) -> Dict: + result = {} + for k, v in d.items(): + if v is None: + continue + result[k] = _filter_none(v) if isinstance(v, dict) else v + return result + + +def format_call_log(log: Optional[List[str]]) -> str: + if not log: + return "" + if len(list(filter(lambda x: x.strip(), log))) == 0: + return "" + return "\nCall log:\n" + "\n".join(log) + "\n" diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_console_message.py b/venv/lib/python3.11/site-packages/playwright/_impl/_console_message.py new file mode 100644 index 0000000..6687919 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_console_message.py @@ -0,0 +1,98 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from asyncio import AbstractEventLoop +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union + +from playwright._impl._api_structures import SourceLocation +from playwright._impl._connection import from_channel, from_nullable_channel +from playwright._impl._js_handle import JSHandle + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + from playwright._impl._worker import Worker + + +class ConsoleMessage: + def __init__( + self, event: Dict, loop: AbstractEventLoop, dispatcher_fiber: Any + ) -> None: + self._event = event + self._loop = loop + self._dispatcher_fiber = dispatcher_fiber + self._page: Optional["Page"] = from_nullable_channel(event.get("page")) + self._worker: Optional["Worker"] = from_nullable_channel(event.get("worker")) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.text + + @property + def type(self) -> Union[ + Literal["assert"], + Literal["clear"], + Literal["count"], + Literal["debug"], + Literal["dir"], + Literal["dirxml"], + Literal["endGroup"], + Literal["error"], + Literal["info"], + Literal["log"], + Literal["profile"], + Literal["profileEnd"], + Literal["startGroup"], + Literal["startGroupCollapsed"], + Literal["table"], + Literal["time"], + Literal["timeEnd"], + Literal["trace"], + Literal["warning"], + ]: + return self._event["type"] + + @property + def text(self) -> str: + return self._event["text"] + + @property + def args(self) -> List[JSHandle]: + return list(map(from_channel, self._event["args"])) + + @property + def location(self) -> SourceLocation: + # Wire format uses `lineNumber`/`columnNumber`; docs expose both `line`/`column` + # (legacy) and `lineNumber`/`columnNumber` (added upstream in 1.60). + loc = self._event["location"] + return { + "url": loc["url"], + "line": loc["lineNumber"], + "column": loc["columnNumber"], + "lineNumber": loc["lineNumber"], + "columnNumber": loc["columnNumber"], + } + + @property + def timestamp(self) -> float: + return self._event["timestamp"] + + @property + def page(self) -> Optional["Page"]: + return self._page + + @property + def worker(self) -> Optional["Worker"]: + return self._worker diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_credentials.py b/venv/lib/python3.11/site-packages/playwright/_impl/_credentials.py new file mode 100644 index 0000000..ad02452 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_credentials.py @@ -0,0 +1,58 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TYPE_CHECKING, List + +from playwright._impl._api_structures import VirtualCredential +from playwright._impl._helper import locals_to_params + +if TYPE_CHECKING: + from playwright._impl._browser_context import BrowserContext + + +class Credentials: + def __init__(self, browser_context: "BrowserContext") -> None: + self._browser_context = browser_context + self._loop = browser_context._loop + self._dispatcher_fiber = browser_context._dispatcher_fiber + + async def install(self) -> None: + await self._browser_context._channel.send("credentialsInstall", None) + + async def create( + self, + rpId: str, + id: str = None, + userHandle: str = None, + privateKey: str = None, + publicKey: str = None, + signCount: int = None, + ) -> VirtualCredential: + result = await self._browser_context._channel.send_return_as_dict( + "credentialsCreate", None, locals_to_params(locals()) + ) + return (result or {})["credential"] + + async def delete(self, id: str) -> None: + await self._browser_context._channel.send("credentialsDelete", None, {"id": id}) + + async def get( + self, + rpId: str = None, + id: str = None, + ) -> List[VirtualCredential]: + result = await self._browser_context._channel.send_return_as_dict( + "credentialsGet", None, locals_to_params(locals()) + ) + return (result or {}).get("credentials", []) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_debugger.py b/venv/lib/python3.11/site-packages/playwright/_impl/_debugger.py new file mode 100644 index 0000000..36e4bd9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_debugger.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import SimpleNamespace +from typing import Any, Dict, Optional + +from playwright._impl._api_structures import DebuggerLocation, DebuggerPausedDetails +from playwright._impl._connection import ChannelOwner + + +class Debugger(ChannelOwner): + Events = SimpleNamespace( + PausedStateChanged="pausedstatechanged", + ) + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._paused_details: Optional[DebuggerPausedDetails] = None + self._channel.on( + "pausedStateChanged", lambda params: self._on_paused_state_changed(params) + ) + + def _on_paused_state_changed(self, params: Dict[str, Any]) -> None: + self._paused_details = params.get("pausedDetails") + self.emit(Debugger.Events.PausedStateChanged) + + async def request_pause(self) -> None: + await self._channel.send("requestPause", None) + + async def resume(self) -> None: + await self._channel.send("resume", None) + + async def next(self) -> None: + await self._channel.send("next", None) + + async def run_to(self, location: DebuggerLocation) -> None: + await self._channel.send("runTo", None, {"location": location}) + + @property + def paused_details(self) -> Optional[DebuggerPausedDetails]: + return self._paused_details diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_dialog.py b/venv/lib/python3.11/site-packages/playwright/_impl/_dialog.py new file mode 100644 index 0000000..f6750e3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_dialog.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TYPE_CHECKING, Dict, Optional + +from playwright._impl._connection import ChannelOwner, from_nullable_channel +from playwright._impl._errors import is_target_closed_error +from playwright._impl._helper import locals_to_params + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + + +class Dialog(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._page: Optional["Page"] = from_nullable_channel(initializer.get("page")) + + def __repr__(self) -> str: + return f"" + + @property + def type(self) -> str: + return self._initializer["type"] + + @property + def message(self) -> str: + return self._initializer["message"] + + @property + def default_value(self) -> str: + return self._initializer["defaultValue"] + + @property + def page(self) -> Optional["Page"]: + return self._page + + async def accept(self, promptText: str = None) -> None: + await self._channel.send("accept", None, locals_to_params(locals())) + + async def dismiss(self) -> None: + try: + await self._channel.send( + "dismiss", + None, + ) + except Exception as e: + if is_target_closed_error(e): + return + raise diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_disposable.py b/venv/lib/python3.11/site-packages/playwright/_impl/_disposable.py new file mode 100644 index 0000000..8035c92 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_disposable.py @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import traceback +from typing import Any, Awaitable, Callable, Coroutine, Dict + +from playwright._impl._connection import ChannelOwner, _capture_stack_trace +from playwright._impl._errors import Error, is_target_closed_error +from playwright._impl._greenlets import connection_closed_error, wait_for_future + + +class Disposable(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + + async def dispose(self) -> None: + try: + await self._channel.send( + "dispose", + None, + ) + except Exception as e: + if not is_target_closed_error(e): + raise e + + async def close(self) -> None: + await self.dispose() + + def __repr__(self) -> str: + return "" + + +class DisposableStub: + def __init__( + self, + dispose_fn: Callable[[], Awaitable[None]], + parent: ChannelOwner, + ) -> None: + self._dispose_fn = dispose_fn + self._loop = parent._loop + self._dispatcher_fiber = parent._dispatcher_fiber + + async def dispose(self) -> None: + await self._dispose_fn() + + async def __aenter__(self) -> "DisposableStub": + return self + + async def __aexit__(self, *args: object) -> None: + await self.dispose() + + def __enter__(self) -> "DisposableStub": + return self + + def __exit__(self, *args: object) -> None: + self._sync(self.dispose()) + + def _sync(self, coro: Coroutine[Any, Any, Any]) -> object: + __tracebackhide__ = True + if self._loop.is_closed(): + coro.close() + raise Error("Event loop is closed! Is Playwright already stopped?") + if self._dispatcher_fiber.dead: + coro.close() + raise connection_closed_error() + task = self._loop.create_task(coro) + setattr( + task, + "__pw_stack__", + _capture_stack_trace(), + ) + setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)) + wait_for_future(self._loop, self._dispatcher_fiber, task) + return task.result() + + async def close(self) -> None: + await self.dispose() + + def __repr__(self) -> str: + return "" diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_download.py b/venv/lib/python3.11/site-packages/playwright/_impl/_download.py new file mode 100644 index 0000000..ffaf5ca --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_download.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +from pathlib import Path +from typing import TYPE_CHECKING, Optional, Union + +from playwright._impl._artifact import Artifact + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + + +class Download: + def __init__( + self, page: "Page", url: str, suggested_filename: str, artifact: Artifact + ) -> None: + self._page = page + self._loop = page._loop + self._dispatcher_fiber = page._dispatcher_fiber + self._url = url + self._suggested_filename = suggested_filename + self._artifact = artifact + + def __repr__(self) -> str: + return f"" + + @property + def page(self) -> "Page": + return self._page + + @property + def url(self) -> str: + return self._url + + @property + def suggested_filename(self) -> str: + return self._suggested_filename + + async def delete(self) -> None: + await self._artifact.delete() + + async def failure(self) -> Optional[str]: + return await self._artifact.failure() + + async def path(self) -> pathlib.Path: + return await self._artifact.path_after_finished() + + async def save_as(self, path: Union[str, Path]) -> None: + await self._artifact.save_as(path) + + async def cancel(self) -> None: + return await self._artifact.cancel() diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_driver.py b/venv/lib/python3.11/site-packages/playwright/_impl/_driver.py new file mode 100644 index 0000000..22b53b8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_driver.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import os +import sys +from pathlib import Path +from typing import Tuple + +import playwright +from playwright._repo_version import version + + +def compute_driver_executable() -> Tuple[str, str]: + driver_path = Path(inspect.getfile(playwright)).parent / "driver" + cli_path = str(driver_path / "package" / "cli.js") + if sys.platform == "win32": + return ( + os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node.exe")), + cli_path, + ) + return (os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node")), cli_path) + + +def get_driver_env() -> dict: + env = os.environ.copy() + env["PW_LANG_NAME"] = "python" + env["PW_LANG_NAME_VERSION"] = f"{sys.version_info.major}.{sys.version_info.minor}" + env["PW_CLI_DISPLAY_VERSION"] = version + return env diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_element_handle.py b/venv/lib/python3.11/site-packages/playwright/_impl/_element_handle.py new file mode 100644 index 0000000..2284b96 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_element_handle.py @@ -0,0 +1,480 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Literal, + Optional, + Sequence, + Union, + cast, +) + +from playwright._impl._api_structures import FilePayload, FloatRect, Position +from playwright._impl._connection import ChannelOwner, from_nullable_channel +from playwright._impl._helper import ( + Error, + KeyboardModifier, + MouseButton, + async_writefile, + locals_to_params, + make_dirs_for_file, +) +from playwright._impl._js_handle import ( + JSHandle, + Serializable, + parse_result, + serialize_argument, +) +from playwright._impl._set_input_files_helpers import convert_input_files + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._frame import Frame + from playwright._impl._locator import Locator + + +class ElementHandle(JSHandle): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._frame = cast("Frame", parent) + + async def _createSelectorForTest(self, name: str) -> Optional[str]: + return await self._channel.send( + "createSelectorForTest", self._frame._timeout, dict(name=name) + ) + + def as_element(self) -> Optional["ElementHandle"]: + return self + + async def owner_frame(self) -> Optional["Frame"]: + return from_nullable_channel(await self._channel.send("ownerFrame", None)) + + async def content_frame(self) -> Optional["Frame"]: + return from_nullable_channel(await self._channel.send("contentFrame", None)) + + async def get_attribute(self, name: str) -> Optional[str]: + return await self._channel.send("getAttribute", None, dict(name=name)) + + async def text_content(self) -> Optional[str]: + return await self._channel.send("textContent", None) + + async def inner_text(self) -> str: + return await self._channel.send("innerText", None) + + async def inner_html(self) -> str: + return await self._channel.send("innerHTML", None) + + async def is_checked(self) -> bool: + return await self._channel.send("isChecked", None) + + async def is_disabled(self) -> bool: + return await self._channel.send("isDisabled", None) + + async def is_editable(self) -> bool: + return await self._channel.send("isEditable", None) + + async def is_enabled(self) -> bool: + return await self._channel.send("isEnabled", None) + + async def is_hidden(self) -> bool: + return await self._channel.send("isHidden", None) + + async def is_visible(self) -> bool: + return await self._channel.send("isVisible", None) + + async def dispatch_event(self, type: str, eventInit: Dict = None) -> None: + await self._channel.send( + "dispatchEvent", + None, + dict(type=type, eventInit=serialize_argument(eventInit)), + ) + + async def scroll_into_view_if_needed(self, timeout: float = None) -> None: + await self._channel.send( + "scrollIntoViewIfNeeded", self._frame._timeout, locals_to_params(locals()) + ) + + async def hover( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "hover", self._frame._timeout, locals_to_params(locals()) + ) + + async def click( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "click", self._frame._timeout, locals_to_params(locals()) + ) + + async def dblclick( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "dblclick", self._frame._timeout, locals_to_params(locals()) + ) + + async def select_option( + self, + value: Union[str, Sequence[str]] = None, + index: Union[int, Sequence[int]] = None, + label: Union[str, Sequence[str]] = None, + element: Union["ElementHandle", Sequence["ElementHandle"]] = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + ) -> List[str]: + params = locals_to_params( + dict( + timeout=timeout, + force=force, + **convert_select_option_values(value, index, label, element), + ) + ) + return await self._channel.send("selectOption", self._frame._timeout, params) + + async def tap( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "tap", self._frame._timeout, locals_to_params(locals()) + ) + + async def fill( + self, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + ) -> None: + await self._channel.send( + "fill", self._frame._timeout, locals_to_params(locals()) + ) + + async def select_text(self, force: bool = None, timeout: float = None) -> None: + await self._channel.send( + "selectText", self._frame._timeout, locals_to_params(locals()) + ) + + async def input_value(self, timeout: float = None) -> str: + return await self._channel.send( + "inputValue", self._frame._timeout, locals_to_params(locals()) + ) + + async def set_input_files( + self, + files: Union[ + str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] + ], + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + frame = await self.owner_frame() + if not frame: + raise Error("Cannot set input files to detached element") + converted = await convert_input_files(files, frame.page.context) + await self._channel.send( + "setInputFiles", + self._frame._timeout, + { + "timeout": timeout, + **converted, + }, + ) + + async def focus(self) -> None: + await self._channel.send("focus", None) + + async def type( + self, + text: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + await self._channel.send( + "type", self._frame._timeout, locals_to_params(locals()) + ) + + async def press( + self, + key: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + await self._channel.send( + "press", self._frame._timeout, locals_to_params(locals()) + ) + + async def set_checked( + self, + checked: bool, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + if checked: + await self.check( + position=position, + timeout=timeout, + force=force, + trial=trial, + scroll=scroll, + ) + else: + await self.uncheck( + position=position, + timeout=timeout, + force=force, + trial=trial, + scroll=scroll, + ) + + async def check( + self, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "check", self._frame._timeout, locals_to_params(locals()) + ) + + async def uncheck( + self, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "uncheck", self._frame._timeout, locals_to_params(locals()) + ) + + async def bounding_box(self) -> Optional[FloatRect]: + return await self._channel.send("boundingBox", None) + + async def screenshot( + self, + timeout: float = None, + type: Literal["jpeg", "png", "webp"] = None, + path: Union[str, Path] = None, + quality: int = None, + omitBackground: bool = None, + animations: Literal["allow", "disabled"] = None, + caret: Literal["hide", "initial"] = None, + scale: Literal["css", "device"] = None, + mask: Sequence["Locator"] = None, + maskColor: str = None, + style: str = None, + ) -> bytes: + params = locals_to_params(locals()) + if "path" in params: + if "type" not in params: + params["type"] = determine_screenshot_type(params["path"]) + del params["path"] + if "mask" in params: + params["mask"] = list( + map( + lambda locator: ( + { + "frame": locator._frame._channel, + "selector": locator._selector, + } + ), + params["mask"], + ) + ) + encoded_binary = await self._channel.send( + "screenshot", self._frame._timeout, params + ) + decoded_binary = base64.b64decode(encoded_binary) + if path: + make_dirs_for_file(path) + await async_writefile(path, decoded_binary) + return decoded_binary + + async def query_selector(self, selector: str) -> Optional["ElementHandle"]: + return from_nullable_channel( + await self._channel.send("querySelector", None, dict(selector=selector)) + ) + + async def query_selector_all(self, selector: str) -> List["ElementHandle"]: + return list( + map( + cast(Callable[[Any], Any], from_nullable_channel), + await self._channel.send( + "querySelectorAll", None, dict(selector=selector) + ), + ) + ) + + async def eval_on_selector( + self, + selector: str, + expression: str, + arg: Serializable = None, + ) -> Any: + return parse_result( + await self._channel.send( + "evalOnSelector", + None, + dict( + selector=selector, + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def eval_on_selector_all( + self, + selector: str, + expression: str, + arg: Serializable = None, + ) -> Any: + return parse_result( + await self._channel.send( + "evalOnSelectorAll", + None, + dict( + selector=selector, + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def wait_for_element_state( + self, + state: Literal[ + "disabled", "editable", "enabled", "hidden", "stable", "visible" + ], + timeout: float = None, + ) -> None: + await self._channel.send( + "waitForElementState", self._frame._timeout, locals_to_params(locals()) + ) + + async def wait_for_selector( + self, + selector: str, + state: Literal["attached", "detached", "hidden", "visible"] = None, + timeout: float = None, + strict: bool = None, + ) -> Optional["ElementHandle"]: + return from_nullable_channel( + await self._channel.send( + "waitForSelector", self._frame._timeout, locals_to_params(locals()) + ) + ) + + +def convert_select_option_values( + value: Union[str, Sequence[str]] = None, + index: Union[int, Sequence[int]] = None, + label: Union[str, Sequence[str]] = None, + element: Union["ElementHandle", Sequence["ElementHandle"]] = None, +) -> Any: + if value is None and index is None and label is None and element is None: + return {} + + options: Any = None + elements: Any = None + if value is not None: + if isinstance(value, str): + value = [value] + options = (options or []) + list(map(lambda e: dict(valueOrLabel=e), value)) + if index is not None: + if isinstance(index, int): + index = [index] + options = (options or []) + list(map(lambda e: dict(index=e), index)) + if label is not None: + if isinstance(label, str): + label = [label] + options = (options or []) + list(map(lambda e: dict(label=e), label)) + if element: + if isinstance(element, ElementHandle): + element = [element] + elements = list(map(lambda e: e._channel, element)) + + return dict(options=options, elements=elements) + + +def determine_screenshot_type(path: Union[str, Path]) -> Literal["jpeg", "png", "webp"]: + # Detect by file extension rather than mimetypes.guess_type, whose result is + # OS-dependent (e.g. Windows does not register image/webp), mirroring + # upstream's getMimeTypeForPath extension map: + # https://github.com/microsoft/playwright/blob/e0e814deed7b0a4c4d2bdf98481e6be7419cda16/packages/isomorphic/mimeType.ts#L28 + suffix = Path(path).suffix.lower() + if suffix == ".png": + return "png" + if suffix in (".jpeg", ".jpg"): + return "jpeg" + if suffix == ".webp": + return "webp" + raise Error(f'path: unsupported mime type "{suffix}"') diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_errors.py b/venv/lib/python3.11/site-packages/playwright/_impl/_errors.py new file mode 100644 index 0000000..9630a23 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_errors.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# These are types that we use in the API. They are public and are a part of the +# stable API. + + +from typing import Any, List, Optional + + +def is_target_closed_error(error: Exception) -> bool: + return isinstance(error, TargetClosedError) + + +class Error(Exception): + def __init__(self, message: str) -> None: + self._message = message + self._name: Optional[str] = None + self._stack: Optional[str] = None + self._details: Optional[Any] = None + self._log: Optional[List[str]] = None + super().__init__(message) + + @property + def message(self) -> str: + return self._message + + @property + def name(self) -> Optional[str]: + return self._name + + @property + def stack(self) -> Optional[str]: + return self._stack + + +class TimeoutError(Error): + pass + + +class TargetClosedError(Error): + def __init__(self, message: str = None) -> None: + super().__init__(message or "Target page, context or browser has been closed") + + +def rewrite_error(error: Exception, message: str) -> Exception: + rewritten_exc = type(error)(message) + if isinstance(rewritten_exc, Error) and isinstance(error, Error): + rewritten_exc._name = error.name + rewritten_exc._stack = error.stack + rewritten_exc._details = error._details + rewritten_exc._log = error._log + return rewritten_exc diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_event_context_manager.py b/venv/lib/python3.11/site-packages/playwright/_impl/_event_context_manager.py new file mode 100644 index 0000000..13191f0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_event_context_manager.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from typing import Any, Generic, TypeVar + +T = TypeVar("T") + + +class EventContextManagerImpl(Generic[T]): + def __init__(self, future: asyncio.Future) -> None: + self._future: asyncio.Future = future + + @property + def future(self) -> asyncio.Future: + return self._future + + async def __aenter__(self) -> asyncio.Future: + return self._future + + async def __aexit__(self, *args: Any) -> None: + await self._future diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_fetch.py b/venv/lib/python3.11/site-packages/playwright/_impl/_fetch.py new file mode 100644 index 0000000..624d5f1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_fetch.py @@ -0,0 +1,656 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import json +import mimetypes +import pathlib +import typing +from pathlib import Path +from typing import Any, Dict, List, Optional, Pattern, Sequence, Union, cast + +import playwright._impl._network as network +from playwright._impl._api_structures import ( + ClientCertificate, + Cookie, + FilePayload, + HttpCredentials, + ProxySettings, + RemoteAddr, + ResourceTiming, + SecurityDetails, + SetCookieParam, + StorageState, +) +from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._errors import is_target_closed_error +from playwright._impl._form_data import FormData +from playwright._impl._helper import ( + Error, + NameValue, + TargetClosedError, + TimeoutSettings, + async_readfile, + async_writefile, + is_file_payload, + locals_to_params, + object_to_array, + to_impl, +) +from playwright._impl._internal_structures import FormField, Headers, ServerFilePayload +from playwright._impl._network import ( + serialize_headers, + to_clear_cookies_params, + to_client_certificates_protocol, + to_http_credentials_protocol, +) +from playwright._impl._tracing import Tracing + +if typing.TYPE_CHECKING: + from playwright._impl._playwright import Playwright + + +FormType = Union[Dict[str, Union[bool, float, str]], FormData] +DataType = Union[Any, bytes, str] +MultipartType = Union[Dict[str, Union[bytes, bool, float, str, FilePayload]], FormData] +ParamsType = Union[Dict[str, Union[bool, float, str]], str] + + +class APIRequest: + def __init__(self, playwright: "Playwright") -> None: + self.playwright = playwright + self._loop = playwright._loop + self._dispatcher_fiber = playwright._connection._dispatcher_fiber + + async def new_context( + self, + baseURL: str = None, + extraHTTPHeaders: Dict[str, str] = None, + httpCredentials: Union[HttpCredentials, Sequence[HttpCredentials]] = None, + ignoreHTTPSErrors: bool = None, + proxy: ProxySettings = None, + userAgent: str = None, + timeout: float = None, + storageState: Union[StorageState, str, Path] = None, + clientCertificates: List[ClientCertificate] = None, + failOnStatusCode: bool = None, + maxRedirects: int = None, + ) -> "APIRequestContext": + params = locals_to_params(locals()) + if "storageState" in params: + storage_state = params["storageState"] + if not isinstance(storage_state, dict) and storage_state: + params["storageState"] = json.loads( + (await async_readfile(storage_state)).decode() + ) + if "extraHTTPHeaders" in params: + params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) + params["clientCertificates"] = await to_client_certificates_protocol( + params.get("clientCertificates") + ) + if "httpCredentials" in params: + params["httpCredentials"] = to_http_credentials_protocol( + params["httpCredentials"] + ) + context = cast( + APIRequestContext, + from_channel( + await self.playwright._channel.send("newRequest", None, params) + ), + ) + context._timeout_settings.set_default_timeout(timeout) + return context + + +class APIRequestContext(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._tracing: Tracing = from_channel(initializer["tracing"]) + self._close_reason: Optional[str] = None + self._timeout_settings = TimeoutSettings(None) + + async def dispose(self, reason: str = None) -> None: + self._close_reason = reason + await self._tracing._export_all_hars() + try: + await self._channel.send("dispose", None, {"reason": reason}) + except Error as e: + if is_target_closed_error(e): + return + raise e + self._tracing._reset_stack_counter() + + @property + def tracing(self) -> Tracing: + return self._tracing + + async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: + if urls is None: + urls = [] + if isinstance(urls, str): + urls = [urls] + return await self._channel.send("cookies", None, dict(urls=urls)) + + async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None: + await self._channel.send("addCookies", None, dict(cookies=cookies)) + + async def clear_cookies( + self, + name: Union[str, Pattern[str]] = None, + domain: Union[str, Pattern[str]] = None, + path: Union[str, Pattern[str]] = None, + ) -> None: + await self._channel.send( + "clearCookies", None, to_clear_cookies_params(name, domain, path) + ) + + async def delete( + self, + url: str, + params: ParamsType = None, + headers: Headers = None, + data: DataType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + return await self.fetch( + url, + method="DELETE", + params=params, + headers=headers, + data=data, + form=form, + multipart=multipart, + timeout=timeout, + failOnStatusCode=failOnStatusCode, + ignoreHTTPSErrors=ignoreHTTPSErrors, + maxRedirects=maxRedirects, + maxRetries=maxRetries, + ) + + async def head( + self, + url: str, + params: ParamsType = None, + headers: Headers = None, + data: DataType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + return await self.fetch( + url, + method="HEAD", + params=params, + headers=headers, + data=data, + form=form, + multipart=multipart, + timeout=timeout, + failOnStatusCode=failOnStatusCode, + ignoreHTTPSErrors=ignoreHTTPSErrors, + maxRedirects=maxRedirects, + maxRetries=maxRetries, + ) + + async def get( + self, + url: str, + params: ParamsType = None, + headers: Headers = None, + data: DataType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + return await self.fetch( + url, + method="GET", + params=params, + headers=headers, + data=data, + form=form, + multipart=multipart, + timeout=timeout, + failOnStatusCode=failOnStatusCode, + ignoreHTTPSErrors=ignoreHTTPSErrors, + maxRedirects=maxRedirects, + maxRetries=maxRetries, + ) + + async def patch( + self, + url: str, + params: ParamsType = None, + headers: Headers = None, + data: DataType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + return await self.fetch( + url, + method="PATCH", + params=params, + headers=headers, + data=data, + form=form, + multipart=multipart, + timeout=timeout, + failOnStatusCode=failOnStatusCode, + ignoreHTTPSErrors=ignoreHTTPSErrors, + maxRedirects=maxRedirects, + maxRetries=maxRetries, + ) + + async def put( + self, + url: str, + params: ParamsType = None, + headers: Headers = None, + data: DataType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + return await self.fetch( + url, + method="PUT", + params=params, + headers=headers, + data=data, + form=form, + multipart=multipart, + timeout=timeout, + failOnStatusCode=failOnStatusCode, + ignoreHTTPSErrors=ignoreHTTPSErrors, + maxRedirects=maxRedirects, + maxRetries=maxRetries, + ) + + async def post( + self, + url: str, + params: ParamsType = None, + headers: Headers = None, + data: DataType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + return await self.fetch( + url, + method="POST", + params=params, + headers=headers, + data=data, + form=form, + multipart=multipart, + timeout=timeout, + failOnStatusCode=failOnStatusCode, + ignoreHTTPSErrors=ignoreHTTPSErrors, + maxRedirects=maxRedirects, + maxRetries=maxRetries, + ) + + async def fetch( + self, + urlOrRequest: Union[str, network.Request], + params: ParamsType = None, + method: str = None, + headers: Headers = None, + data: DataType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + url = urlOrRequest if isinstance(urlOrRequest, str) else None + request = ( + cast(network.Request, to_impl(urlOrRequest)) + if isinstance(to_impl(urlOrRequest), network.Request) + else None + ) + if not request and not isinstance(urlOrRequest, str): + raise Error("First argument must be either URL string or Request") + return await self._inner_fetch( + request, + url, + method, + headers, + data, + params, + form, + multipart, + timeout, + failOnStatusCode, + ignoreHTTPSErrors, + maxRedirects, + maxRetries, + ) + + async def _inner_fetch( + self, + request: Optional[network.Request], + url: Optional[str], + method: str = None, + headers: Headers = None, + data: DataType = None, + params: ParamsType = None, + form: FormType = None, + multipart: MultipartType = None, + timeout: float = None, + failOnStatusCode: bool = None, + ignoreHTTPSErrors: bool = None, + maxRedirects: int = None, + maxRetries: int = None, + ) -> "APIResponse": + if self._close_reason: + raise TargetClosedError(self._close_reason) + if ( + (1 if data is not None else 0) + + (1 if form is not None else 0) + + (1 if multipart is not None else 0) + ) > 1: + raise Error("Only one of 'data', 'form' or 'multipart' can be specified") + if maxRedirects is not None and maxRedirects < 0: + raise Error("'max_redirects' must be greater than or equal to '0'") + if maxRetries is not None and maxRetries < 0: + raise Error("'max_retries' must be greater than or equal to '0'") + url = url or (request.url if request else url) + method = method or (request.method if request else "GET") + # Cannot call allHeaders() here as the request may be paused inside route handler. + headers_obj = headers or (request.headers if request else None) + serialized_headers = serialize_headers(headers_obj) if headers_obj else None + json_data: Any = None + form_data: Optional[List[NameValue]] = None + multipart_data: Optional[List[FormField]] = None + post_data_buffer: Optional[bytes] = None + if data is not None: + if isinstance(data, str): + if is_json_content_type(serialized_headers): + json_data = data if is_json_parsable(data) else json.dumps(data) + else: + post_data_buffer = data.encode() + elif isinstance(data, bytes): + post_data_buffer = data + elif isinstance(data, (dict, list, int, float, bool)): + try: + # NaN and Infinity are not valid JSON. + json_data = json.dumps(data, allow_nan=False) + except ValueError as e: + raise Error(str(e)) from None + else: + raise Error(f"Unsupported 'data' type: {type(data)}") + elif form is not None: + if isinstance(form, FormData): + form_data = [] + for fd_name, fd_value in form._fields: + if isinstance(fd_value, (pathlib.Path, dict)): + raise Error( + f"Form field {fd_name!r} must be a string, number or boolean. Use 'multipart' for file uploads." + ) + form_data.append(NameValue(name=fd_name, value=str(fd_value))) + else: + form_data = object_to_array(form) or [] + elif multipart is not None: + multipart_data = [] + if isinstance(multipart, FormData): + for fd_name, fd_value in multipart._fields: + multipart_data.append( + await _form_data_field_to_form_field(fd_name, fd_value) + ) + else: + for name, value in multipart.items(): + multipart_data.append( + await _form_data_field_to_form_field(name, value) + ) + if ( + post_data_buffer is None + and json_data is None + and form_data is None + and multipart_data is None + ): + post_data_buffer = request.post_data_buffer if request else None + post_data = ( + base64.b64encode(post_data_buffer).decode() if post_data_buffer else None + ) + + response = await self._channel.send( + "fetch", + self._timeout_settings.timeout, + { + "url": url, + "timeout": timeout, + "params": object_to_array(params) if isinstance(params, dict) else None, + "encodedParams": params if isinstance(params, str) else None, + "method": method, + "headers": serialized_headers, + "postData": post_data, + "jsonData": json_data, + "formData": form_data, + "multipartData": multipart_data, + "failOnStatusCode": failOnStatusCode, + "ignoreHTTPSErrors": ignoreHTTPSErrors, + "maxRedirects": maxRedirects, + "maxRetries": maxRetries, + }, + ) + return APIResponse(self, response) + + async def storage_state( + self, + path: Union[pathlib.Path, str] = None, + indexedDB: bool = None, + opfs: bool = None, + ) -> StorageState: + result = await self._channel.send_return_as_dict( + "storageState", None, {"indexedDB": indexedDB, "opfs": opfs} + ) + if path: + await async_writefile(path, json.dumps(result)) + return result + + +def file_payload_to_json(payload: FilePayload) -> ServerFilePayload: + return ServerFilePayload( + name=payload["name"], + mimeType=payload["mimeType"], + buffer=base64.b64encode(payload["buffer"]).decode(), + ) + + +async def _form_data_field_to_form_field(name: str, value: Any) -> FormField: + if isinstance(value, pathlib.Path): + mime_type, _ = mimetypes.guess_type(str(value)) + return FormField( + name=name, + file=ServerFilePayload( + name=value.name, + mimeType=mime_type or "application/octet-stream", + buffer=base64.b64encode(await async_readfile(str(value))).decode(), + ), + ) + if is_file_payload(value): + payload = cast(FilePayload, value) + if not isinstance(payload["buffer"], bytes): + raise Error(f"Unexpected buffer type of form field {name!r}") + return FormField(name=name, file=file_payload_to_json(payload)) + if isinstance(value, (str, int, float, bool)): + return FormField(name=name, value=str(value)) + raise Error(f"Unsupported form field {name!r} value type: {type(value).__name__}") + + +class APIResponse: + def __init__(self, context: APIRequestContext, initializer: Dict) -> None: + self._loop = context._loop + self._dispatcher_fiber = context._connection._dispatcher_fiber + self._request = context + self._initializer = initializer + self._headers = network.RawHeaders(initializer["headers"]) + + def __repr__(self) -> str: + return f"" + + @property + def ok(self) -> bool: + return self.status >= 200 and self.status <= 299 + + @property + def url(self) -> str: + return self._initializer["url"] + + @property + def status(self) -> int: + return self._initializer["status"] + + @property + def status_text(self) -> str: + return self._initializer["statusText"] + + @property + def headers(self) -> Headers: + return self._headers.headers() + + @property + def headers_array(self) -> network.HeadersArray: + return self._headers.headers_array() + + @property + def timing(self) -> ResourceTiming: + return cast( + ResourceTiming, + { + "startTime": -1, + "domainLookupStart": -1, + "domainLookupEnd": -1, + "connectStart": -1, + "secureConnectionStart": -1, + "connectEnd": -1, + "requestStart": -1, + "responseStart": -1, + **self._initializer.get("timing", {}), + "responseEnd": self._initializer.get("responseEndTiming", -1), + }, + ) + + async def body(self) -> bytes: + try: + result = await self._request._connection.wrap_api_call( + lambda: self._request._channel.send_return_as_dict( + "fetchResponseBody", + None, + { + "fetchUid": self._fetch_uid, + }, + ), + True, + ) + if result is None: + raise Error("Response has been disposed") + return base64.b64decode(result["binary"]) + except Error as exc: + if is_target_closed_error(exc): + raise Error("Response has been disposed") + raise exc + + async def text(self) -> str: + content = await self.body() + return content.decode() + + async def json(self) -> Any: + content = await self.text() + return json.loads(content) + + async def security_details(self) -> Optional[SecurityDetails]: + return self._initializer.get("securityDetails") or None + + async def server_addr(self) -> Optional[RemoteAddr]: + return self._initializer.get("serverAddr") or None + + async def dispose(self) -> None: + await self._request._channel.send( + "disposeAPIResponse", + None, + { + "fetchUid": self._fetch_uid, + }, + ) + + @property + def _fetch_uid(self) -> str: + return self._initializer["fetchUid"] + + async def _fetch_log(self) -> List[str]: + return await self._request._channel.send( + "fetchLog", + None, + { + "fetchUid": self._fetch_uid, + }, + ) + + +def is_json_content_type(headers: network.HeadersArray = None) -> bool: + if not headers: + return False + for header in headers: + if header["name"].lower() == "content-type": + return header["value"].startswith("application/json") + return False + + +def is_json_parsable(value: Any) -> bool: + if not isinstance(value, str): + return False + try: + # NaN and Infinity are not valid JSON, unlike what json.loads() accepts by default. + json.loads(value, parse_constant=_reject_json_constant) + return True + except ValueError: + return False + + +def _reject_json_constant(value: str) -> Any: + raise ValueError(f"{value} is not valid JSON") diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_file_chooser.py b/venv/lib/python3.11/site-packages/playwright/_impl/_file_chooser.py new file mode 100644 index 0000000..951919d --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_file_chooser.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import TYPE_CHECKING, Sequence, Union + +from playwright._impl._api_structures import FilePayload + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._element_handle import ElementHandle + from playwright._impl._page import Page + + +class FileChooser: + def __init__( + self, page: "Page", element_handle: "ElementHandle", is_multiple: bool + ) -> None: + self._page = page + self._loop = page._loop + self._dispatcher_fiber = page._dispatcher_fiber + self._element_handle = element_handle + self._is_multiple = is_multiple + + def __repr__(self) -> str: + return f"" + + @property + def page(self) -> "Page": + return self._page + + @property + def element(self) -> "ElementHandle": + return self._element_handle + + def is_multiple(self) -> bool: + return self._is_multiple + + async def set_files( + self, + files: Union[ + str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] + ], + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + await self._element_handle.set_input_files(files, timeout, noWaitAfter) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_form_data.py b/venv/lib/python3.11/site-packages/playwright/_impl/_form_data.py new file mode 100644 index 0000000..384806a --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_form_data.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +from typing import List, Tuple, Union + +from playwright._impl._api_structures import FilePayload + +FormDataValue = Union[bool, float, str, pathlib.Path, FilePayload] + + +class FormData: + def __init__(self) -> None: + self._fields: List[Tuple[str, FormDataValue]] = [] + + def set(self, name: str, value: FormDataValue) -> "FormData": + self._fields = [(n, v) for (n, v) in self._fields if n != name] + self._fields.append((name, value)) + return self + + def append(self, name: str, value: FormDataValue) -> "FormData": + self._fields.append((name, value)) + return self diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_frame.py b/venv/lib/python3.11/site-packages/playwright/_impl/_frame.py new file mode 100644 index 0000000..c0e7815 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_frame.py @@ -0,0 +1,990 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Literal, + Optional, + Pattern, + Sequence, + Set, + Union, + cast, +) + +from pyee import EventEmitter + +from playwright._impl._api_structures import DropPayload, FilePayload, Position +from playwright._impl._connection import ( + ChannelOwner, + from_channel, + from_nullable_channel, +) +from playwright._impl._element_handle import ElementHandle, convert_select_option_values +from playwright._impl._errors import Error +from playwright._impl._event_context_manager import EventContextManagerImpl +from playwright._impl._helper import ( + DocumentLoadState, + FrameNavigatedEvent, + KeyboardModifier, + MouseButton, + TimeoutSettings, + URLMatch, + async_readfile, + locals_to_params, + monotonic_time, + url_matches, +) +from playwright._impl._internal_structures import ( + AriaRole, + FrameExpectOptions, + FrameExpectResult, +) +from playwright._impl._js_handle import ( + JSHandle, + Serializable, + add_source_url_to_script, + parse_result, + parse_value, + serialize_argument, +) +from playwright._impl._locator import ( + ANY_FRAME_SELECTOR, + FrameLocator, + Locator, + get_by_alt_text_selector, + get_by_label_selector, + get_by_placeholder_selector, + get_by_role_selector, + get_by_test_id_selector, + get_by_text_selector, + get_by_title_selector, + test_id_attribute_name, +) +from playwright._impl._network import Response +from playwright._impl._set_input_files_helpers import convert_input_files +from playwright._impl._waiter import Waiter +from playwright._impl._webmcp import WebMCP + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + + +class Frame(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._parent_frame = from_nullable_channel(initializer.get("parentFrame")) + if self._parent_frame: + self._parent_frame._child_frames.append(self) + self._name = initializer["name"] + self._url = initializer["url"] + self._detached = False + self._child_frames: List[Frame] = [] + self._page: Optional[Page] = None + self._load_states: Set[str] = set(initializer["loadStates"]) + self._event_emitter = EventEmitter() + self._webmcp = WebMCP(self) + self._channel.on( + "loadstate", + lambda params: self._on_load_state(params.get("add"), params.get("remove")), + ) + self._channel.on( + "navigated", + lambda params: self._on_frame_navigated(params), + ) + + def __repr__(self) -> str: + return f"" + + def _on_load_state( + self, add: DocumentLoadState = None, remove: DocumentLoadState = None + ) -> None: + if add: + self._load_states.add(add) + self._event_emitter.emit("loadstate", add) + elif remove and remove in self._load_states: + self._load_states.remove(remove) + if not self._parent_frame and add == "load" and self._page: + self._page.emit("load", self._page) + self._page.context.emit("pageload", self._page) + if not self._parent_frame and add == "domcontentloaded" and self._page: + self._page.emit("domcontentloaded", self._page) + + def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None: + self._url = event["url"] + self._name = event["name"] + self._event_emitter.emit("navigated", event) + if "error" not in event and self._page: + self._page.emit("framenavigated", self) + self._page.context.emit("framenavigated", self) + + async def _query_count(self, selector: str) -> int: + return await self._channel.send("queryCount", None, {"selector": selector}) + + @property + def page(self) -> "Page": + assert self._page + return self._page + + async def goto( + self, + url: str, + timeout: float = None, + waitUntil: DocumentLoadState = None, + referer: str = None, + ) -> Optional[Response]: + return cast( + Optional[Response], + from_nullable_channel( + await self._channel.send( + "goto", self._navigation_timeout, locals_to_params(locals()) + ) + ), + ) + + def _setup_navigation_waiter(self, wait_name: str, timeout: float = None) -> Waiter: + assert self._page + waiter = Waiter(self._page, f"frame.{wait_name}") + waiter.reject_on_event( + self._page, + "close", + lambda: cast("Page", self._page)._close_error_with_reason(), + ) + waiter.reject_on_event( + self._page, "crash", Error("Navigation failed because page crashed!") + ) + waiter.reject_on_event( + self._page, + "framedetached", + Error("Navigating frame was detached!"), + lambda frame: frame == self, + ) + timeout = self._page._timeout_settings.navigation_timeout(timeout) + waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.") + return waiter + + async def _expect( + self, + selector: Optional[str], + expression: str, + options: FrameExpectOptions, + title: str = None, + ) -> FrameExpectResult: + if "expectedValue" in options: + options["expectedValue"] = serialize_argument(options["expectedValue"]) + try: + await self._channel.send( + "expect", + self._timeout, + { + "selector": selector, + "expression": expression, + **options, + }, + title=title, + ) + return {"matches": not options.get("isNot")} + except Error as e: + if not e._details: + raise e + details = cast(Dict[str, Any], e._details) + received = details.get("received") + if received: + received = { + "value": ( + parse_value(received["value"]) if "value" in received else None + ), + "ariaSnapshot": received.get("ariaSnapshot"), + } + return { + "matches": bool(options.get("isNot")), + "received": received, + "timedOut": details.get("timedOut"), + "errorMessage": ( + "Error: " + details["customErrorMessage"] + if details.get("customErrorMessage") + else None + ), + "log": e._log, + } + + def expect_navigation( + self, + url: URLMatch = None, + waitUntil: DocumentLoadState = None, + timeout: float = None, + ) -> EventContextManagerImpl[Response]: + assert self._page + if not waitUntil: + waitUntil = "load" + + if timeout is None: + timeout = self._page._timeout_settings.navigation_timeout() + deadline = monotonic_time() + timeout + waiter = self._setup_navigation_waiter("expect_navigation", timeout) + + to_url = f' to "{url}"' if url else "" + waiter.log(f"waiting for navigation{to_url} until '{waitUntil}'") + + def predicate(event: Any) -> bool: + # Any failed navigation results in a rejection. + if event.get("error"): + return True + waiter.log(f' navigated to "{event["url"]}"') + return url_matches( + cast("Page", self._page)._browser_context._base_url, + event["url"], + url, + ) + + waiter.wait_for_event( + self._event_emitter, + "navigated", + predicate=predicate, + ) + + async def continuation() -> Optional[Response]: + event = await waiter.result() + if "error" in event: + raise Error(event["error"]) + if waitUntil not in self._load_states: + t = deadline - monotonic_time() + if t > 0: + await self._wait_for_load_state_impl(state=waitUntil, timeout=t) + if "newDocument" in event and "request" in event["newDocument"]: + request = from_channel(event["newDocument"]["request"]) + return await request.response() + return None + + return EventContextManagerImpl(asyncio.create_task(continuation())) + + async def wait_for_url( + self, + url: URLMatch, + waitUntil: DocumentLoadState = None, + timeout: float = None, + ) -> None: + assert self._page + if url_matches(self._page._browser_context._base_url, self.url, url): + await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout) + return + async with self.expect_navigation( + url=url, waitUntil=waitUntil, timeout=timeout + ): + pass + + async def wait_for_load_state( + self, + state: Literal["domcontentloaded", "load", "networkidle"] = None, + timeout: float = None, + ) -> None: + return await self._wait_for_load_state_impl(state, timeout) + + async def _wait_for_load_state_impl( + self, state: DocumentLoadState = None, timeout: float = None + ) -> None: + if not state: + state = "load" + if state not in ("load", "domcontentloaded", "networkidle", "commit"): + raise Error( + "state: expected one of (load|domcontentloaded|networkidle|commit)" + ) + waiter = self._setup_navigation_waiter("wait_for_load_state", timeout) + + if state in self._load_states: + waiter.log(f' not waiting, "{state}" event already fired') + # TODO: align with upstream + waiter._fulfill(None) + else: + + def handle_load_state_event(actual_state: str) -> bool: + waiter.log(f'"{actual_state}" event fired') + return actual_state == state + + waiter.wait_for_event( + self._event_emitter, + "loadstate", + handle_load_state_event, + ) + await waiter.result() + + def _timeout(self, timeout: Optional[float]) -> float: + timeout_settings = ( + self._page._timeout_settings if self._page else TimeoutSettings(None) + ) + return timeout_settings.timeout(timeout) + + def _navigation_timeout(self, timeout: Optional[float]) -> float: + timeout_settings = ( + self._page._timeout_settings if self._page else TimeoutSettings(None) + ) + return timeout_settings.navigation_timeout(timeout) + + async def frame_element(self) -> ElementHandle: + return from_channel(await self._channel.send("frameElement", None)) + + async def evaluate(self, expression: str, arg: Serializable = None) -> Any: + return parse_result( + await self._channel.send( + "evaluateExpression", + None, + dict( + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def evaluate_handle( + self, expression: str, arg: Serializable = None + ) -> JSHandle: + return from_channel( + await self._channel.send( + "evaluateExpressionHandle", + None, + dict( + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def query_selector( + self, selector: str, strict: bool = None + ) -> Optional[ElementHandle]: + return from_nullable_channel( + await self._channel.send("querySelector", None, locals_to_params(locals())) + ) + + async def query_selector_all(self, selector: str) -> List[ElementHandle]: + return list( + map( + from_channel, + await self._channel.send( + "querySelectorAll", None, dict(selector=selector) + ), + ) + ) + + async def wait_for_selector( + self, + selector: str, + strict: bool = None, + timeout: float = None, + state: Literal["attached", "detached", "hidden", "visible"] = None, + ) -> Optional[ElementHandle]: + return from_nullable_channel( + await self._channel.send( + "waitForSelector", self._timeout, locals_to_params(locals()) + ) + ) + + async def is_checked( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._channel.send( + "isChecked", self._timeout, locals_to_params(locals()) + ) + + async def is_disabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._channel.send( + "isDisabled", self._timeout, locals_to_params(locals()) + ) + + async def is_editable( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._channel.send( + "isEditable", self._timeout, locals_to_params(locals()) + ) + + async def is_enabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._channel.send( + "isEnabled", self._timeout, locals_to_params(locals()) + ) + + async def is_hidden(self, selector: str, strict: bool = None) -> bool: + return await self._channel.send( + "isHidden", self._timeout, locals_to_params(locals()) + ) + + async def is_visible(self, selector: str, strict: bool = None) -> bool: + return await self._channel.send( + "isVisible", self._timeout, locals_to_params(locals()) + ) + + async def dispatch_event( + self, + selector: str, + type: str, + eventInit: Dict = None, + strict: bool = None, + timeout: float = None, + ) -> None: + await self._channel.send( + "dispatchEvent", + self._timeout, + locals_to_params( + dict( + selector=selector, + type=type, + eventInit=serialize_argument(eventInit), + strict=strict, + timeout=timeout, + ), + ), + ) + + async def eval_on_selector( + self, + selector: str, + expression: str, + arg: Serializable = None, + strict: bool = None, + ) -> Any: + return parse_result( + await self._channel.send( + "evalOnSelector", + None, + locals_to_params( + dict( + selector=selector, + expression=expression, + arg=serialize_argument(arg), + strict=strict, + ) + ), + ) + ) + + async def eval_on_selector_all( + self, + selector: str, + expression: str, + arg: Serializable = None, + ) -> Any: + return parse_result( + await self._channel.send( + "evalOnSelectorAll", + None, + dict( + selector=selector, + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def content(self, includeShadow: bool = None) -> str: + return await self._channel.send("content", None, locals_to_params(locals())) + + async def set_content( + self, + html: str, + timeout: float = None, + waitUntil: DocumentLoadState = None, + ) -> None: + await self._channel.send( + "setContent", self._navigation_timeout, locals_to_params(locals()) + ) + + @property + def name(self) -> str: + return self._name or "" + + @property + def url(self) -> str: + return self._url or "" + + @property + def webmcp(self) -> WebMCP: + return self._webmcp + + @property + def parent_frame(self) -> Optional["Frame"]: + return self._parent_frame + + @property + def child_frames(self) -> List["Frame"]: + return self._child_frames.copy() + + def is_detached(self) -> bool: + return self._detached + + async def add_script_tag( + self, + url: str = None, + path: Union[str, Path] = None, + content: str = None, + type: str = None, + ) -> ElementHandle: + params = locals_to_params(locals()) + if path: + params["content"] = add_source_url_to_script( + (await async_readfile(path)).decode(), path + ) + del params["path"] + return from_channel(await self._channel.send("addScriptTag", None, params)) + + async def add_style_tag( + self, url: str = None, path: Union[str, Path] = None, content: str = None + ) -> ElementHandle: + params = locals_to_params(locals()) + if path: + params["content"] = ( + (await async_readfile(path)).decode() + + "\n/*# sourceURL=" + + str(Path(path)) + + "*/" + ) + del params["path"] + return from_channel(await self._channel.send("addStyleTag", None, params)) + + async def click( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._click(**locals_to_params(locals())) + + async def _click( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send("click", self._timeout, locals_to_params(locals())) + + async def dblclick( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "dblclick", self._timeout, locals_to_params(locals()), title="Double click" + ) + + async def tap( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send("tap", self._timeout, locals_to_params(locals())) + + async def fill( + self, + selector: str, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + force: bool = None, + ) -> None: + await self._fill(**locals_to_params(locals())) + + async def _fill( + self, + selector: str, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + force: bool = None, + title: str = None, + ) -> None: + await self._channel.send("fill", self._timeout, locals_to_params(locals())) + + def locator( + self, + selector: str, + hasText: Union[str, Pattern[str]] = None, + hasNotText: Union[str, Pattern[str]] = None, + has: Locator = None, + hasNot: Locator = None, + ) -> Locator: + return Locator( + self, + selector, + has_text=hasText, + has_not_text=hasNotText, + has=has, + has_not=hasNot, + ) + + def get_by_alt_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_alt_text_selector(text, exact=exact)) + + def get_by_label( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_label_selector(text, exact=exact)) + + def get_by_placeholder( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_placeholder_selector(text, exact=exact)) + + def get_by_role( + self, + role: AriaRole, + checked: bool = None, + disabled: bool = None, + expanded: bool = None, + includeHidden: bool = None, + level: int = None, + name: Union[str, Pattern[str]] = None, + pressed: bool = None, + selected: bool = None, + exact: bool = None, + description: Union[str, Pattern[str]] = None, + ) -> "Locator": + return self.locator( + get_by_role_selector( + role, + checked=checked, + disabled=disabled, + expanded=expanded, + includeHidden=includeHidden, + level=level, + name=name, + pressed=pressed, + selected=selected, + exact=exact, + description=description, + ) + ) + + def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": + return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId)) + + def get_by_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_text_selector(text, exact=exact)) + + def get_by_title( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_title_selector(text, exact=exact)) + + def frame_locator(self, selector: str = None) -> FrameLocator: + return FrameLocator(self, ANY_FRAME_SELECTOR if selector is None else selector) + + async def focus( + self, selector: str, strict: bool = None, timeout: float = None + ) -> None: + await self._channel.send("focus", self._timeout, locals_to_params(locals())) + + async def text_content( + self, selector: str, strict: bool = None, timeout: float = None + ) -> Optional[str]: + return await self._channel.send( + "textContent", self._timeout, locals_to_params(locals()) + ) + + async def inner_text( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: + return await self._channel.send( + "innerText", self._timeout, locals_to_params(locals()) + ) + + async def inner_html( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: + return await self._channel.send( + "innerHTML", self._timeout, locals_to_params(locals()) + ) + + async def get_attribute( + self, selector: str, name: str, strict: bool = None, timeout: float = None + ) -> Optional[str]: + return await self._channel.send( + "getAttribute", self._timeout, locals_to_params(locals()) + ) + + async def hover( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send("hover", self._timeout, locals_to_params(locals())) + + async def drag_and_drop( + self, + source: str, + target: str, + sourcePosition: Position = None, + targetPosition: Position = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + timeout: float = None, + trial: bool = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send( + "dragAndDrop", self._timeout, locals_to_params(locals()) + ) + + async def select_option( + self, + selector: str, + value: Union[str, Sequence[str]] = None, + index: Union[int, Sequence[int]] = None, + label: Union[str, Sequence[str]] = None, + element: Union["ElementHandle", Sequence["ElementHandle"]] = None, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + force: bool = None, + ) -> List[str]: + params = locals_to_params( + dict( + selector=selector, + timeout=timeout, + strict=strict, + force=force, + **convert_select_option_values(value, index, label, element), + ) + ) + return await self._channel.send("selectOption", self._timeout, params) + + async def input_value( + self, + selector: str, + strict: bool = None, + timeout: float = None, + ) -> str: + return await self._channel.send( + "inputValue", self._timeout, locals_to_params(locals()) + ) + + async def set_input_files( + self, + selector: str, + files: Union[ + str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] + ], + strict: bool = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + converted = await convert_input_files(files, self.page.context) + await self._channel.send( + "setInputFiles", + self._timeout, + { + "selector": selector, + "strict": strict, + "timeout": self._timeout(timeout), + **converted, + }, + ) + + async def _drop( + self, + selector: str, + payload: "DropPayload", + strict: bool = None, + position: Position = None, + timeout: float = None, + ) -> None: + params: Dict[str, Any] = { + "selector": selector, + "strict": strict, + "position": position, + "timeout": self._timeout(timeout), + } + files = payload.get("files") if payload else None + if files is not None: + converted = await convert_input_files(files, self.page.context) + if "directoryStream" in converted or "directoryLocalPath" in converted: + raise Error( + "Dropping a directory is not supported, pass individual files instead." + ) + params.update(converted) + data = payload.get("data") if payload else None + if data is not None: + params["data"] = [{"mimeType": k, "value": v} for k, v in data.items()] + await self._channel.send("drop", self._timeout, params) + + async def type( + self, + selector: str, + text: str, + delay: float = None, + strict: bool = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + await self._channel.send("type", self._timeout, locals_to_params(locals())) + + async def press( + self, + selector: str, + key: str, + delay: float = None, + strict: bool = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + await self._channel.send("press", self._timeout, locals_to_params(locals())) + + async def check( + self, + selector: str, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send("check", self._timeout, locals_to_params(locals())) + + async def uncheck( + self, + selector: str, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + await self._channel.send("uncheck", self._timeout, locals_to_params(locals())) + + async def wait_for_timeout(self, timeout: float) -> None: + await self._channel.send("waitForTimeout", None, {"waitTimeout": timeout}) + + async def wait_for_function( + self, + expression: str, + arg: Serializable = None, + timeout: float = None, + polling: Union[float, Literal["raf"]] = None, + ) -> JSHandle: + if isinstance(polling, str) and polling != "raf": + raise Error(f"Unknown polling option: {polling}") + params = locals_to_params(locals()) + params["arg"] = serialize_argument(arg) + if polling is not None and polling != "raf": + params["pollingInterval"] = polling + return from_channel( + await self._channel.send("waitForFunction", self._timeout, params) + ) + + async def title(self) -> str: + return await self._channel.send("title", None) + + async def set_checked( + self, + selector: str, + checked: bool, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + if checked: + await self.check( + selector=selector, + position=position, + timeout=timeout, + force=force, + strict=strict, + trial=trial, + scroll=scroll, + ) + else: + await self.uncheck( + selector=selector, + position=position, + timeout=timeout, + force=force, + strict=strict, + trial=trial, + scroll=scroll, + ) + + async def _highlight(self, selector: str, style: str = None) -> None: + await self._channel.send( + "highlight", None, {"selector": selector, "style": style} + ) + + async def _hide_highlight(self, selector: str) -> None: + await self._channel.send("hideHighlight", None, {"selector": selector}) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_glob.py b/venv/lib/python3.11/site-packages/playwright/_impl/_glob.py new file mode 100644 index 0000000..b388269 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_glob.py @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping +escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"} + + +def glob_to_regex_pattern(glob: str) -> str: + tokens = ["^"] + in_group = False + + i = 0 + while i < len(glob): + c = glob[i] + if c == "\\" and i + 1 < len(glob): + char = glob[i + 1] + tokens.append("\\" + char if char in escaped_chars else char) + i += 1 + elif c == "*": + char_before = glob[i - 1] if i > 0 else None + star_count = 1 + while i + 1 < len(glob) and glob[i + 1] == "*": + star_count += 1 + i += 1 + if star_count > 1: + char_after = glob[i + 1] if i + 1 < len(glob) else None + if char_after == "/": + if char_before == "/": + tokens.append("((.+/)|)") + else: + tokens.append("(.*/)") + i += 1 + else: + tokens.append("(.*)") + else: + tokens.append("([^/]*)") + else: + if c == "{": + in_group = True + tokens.append("(") + elif c == "}": + in_group = False + tokens.append(")") + elif c == ",": + if in_group: + tokens.append("|") + else: + tokens.append("\\" + c) + else: + tokens.append("\\" + c if c in escaped_chars else c) + i += 1 + + tokens.append("$") + return "".join(tokens) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_greenlets.py b/venv/lib/python3.11/site-packages/playwright/_impl/_greenlets.py new file mode 100644 index 0000000..609c3d7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_greenlets.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import os +from typing import Tuple + +import greenlet + +from playwright._impl._errors import TargetClosedError + + +def _greenlet_trace_callback( + event: str, args: Tuple[greenlet.greenlet, greenlet.greenlet] +) -> None: + if event in ("switch", "throw"): + origin, target = args + print(f"Transfer from {origin} to {target} with {event}") + + +if os.environ.get("INTERNAL_PW_GREENLET_DEBUG"): + greenlet.settrace(_greenlet_trace_callback) + + +class MainGreenlet(greenlet.greenlet): + def __str__(self) -> str: + return "" + + +class RouteGreenlet(greenlet.greenlet): + def __str__(self) -> str: + return "" + + +class LocatorHandlerGreenlet(greenlet.greenlet): + def __str__(self) -> str: + return "" + + +class EventGreenlet(greenlet.greenlet): + def __str__(self) -> str: + return "" + + +def connection_closed_error() -> TargetClosedError: + return TargetClosedError("Playwright connection closed") + + +def wait_for_future( + loop: asyncio.AbstractEventLoop, + dispatcher_fiber: greenlet.greenlet, + future: "asyncio.Future", +) -> None: + __tracebackhide__ = True + g_self = greenlet.getcurrent() + + def resume(_: "asyncio.Future") -> None: + g_self.switch() + + # The wait owns the callback that resumes this greenlet, so that it can + # remove it again when the wait is abandoned. A callback left behind would + # resume this greenlet at an arbitrary later point. + future.add_done_callback(resume) + try: + while not future.done(): + # The dispatcher fiber exits once the connection to the driver ends, + # e.g. when the driver process dies. Nothing can settle the future + # after that, and switching to a dead greenlet returns right away, + # so we would spin. + if dispatcher_fiber.dead: + raise connection_closed_error() + # Raises when a signal handler interrupted the event loop while it + # waited for the driver, see greenlet_main in _context_manager.py. + dispatcher_fiber.switch() + except BaseException: + future.remove_done_callback(resume) + future.cancel() + raise + finally: + # The loop is only running for as long as the dispatcher fiber is alive. + # Also after a signal handler interrupted it: run_forever() cleared this + # on its way out, but the dispatcher is going to run the loop again. + if not dispatcher_fiber.dead: + asyncio._set_running_loop(loop) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_har_router.py b/venv/lib/python3.11/site-packages/playwright/_impl/_har_router.py new file mode 100644 index 0000000..9b92f70 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_har_router.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import base64 +from typing import TYPE_CHECKING, Optional, cast + +from playwright._impl._helper import ( + HarLookupResult, + RouteFromHarNotFoundPolicy, + URLMatch, +) +from playwright._impl._internal_structures import HeadersArray +from playwright._impl._local_utils import LocalUtils + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser_context import BrowserContext + from playwright._impl._network import Route + from playwright._impl._page import Page + + +class HarRouter: + def __init__( + self, + local_utils: LocalUtils, + har_id: str, + not_found_action: RouteFromHarNotFoundPolicy, + url_matcher: Optional[URLMatch] = None, + ) -> None: + self._local_utils: LocalUtils = local_utils + self._har_id: str = har_id + self._not_found_action: RouteFromHarNotFoundPolicy = not_found_action + self._options_url_match: Optional[URLMatch] = url_matcher + + @staticmethod + async def create( + local_utils: LocalUtils, + file: str, + not_found_action: RouteFromHarNotFoundPolicy, + url_matcher: Optional[URLMatch] = None, + ) -> "HarRouter": + har_id = await local_utils._channel.send("harOpen", None, {"file": file}) + return HarRouter( + local_utils=local_utils, + har_id=har_id, + not_found_action=not_found_action, + url_matcher=url_matcher, + ) + + async def _handle(self, route: "Route") -> None: + request = route.request + response: HarLookupResult = await self._local_utils.har_lookup( + harId=self._har_id, + url=request.url, + method=request.method, + headers=await request.headers_array(), + postData=request.post_data_buffer, + isNavigationRequest=request.is_navigation_request(), + ) + action = response["action"] + if action == "redirect": + redirect_url = response["redirectURL"] + assert redirect_url + await route._redirected_navigation_request(redirect_url) + return + + if action == "fulfill": + # If the response status is -1, the request was canceled or stalled, so we just stall it here. + # See https://github.com/microsoft/playwright/issues/29311. + # TODO: it'd be better to abort such requests, but then we likely need to respect the timing, + # because the request might have been stalled for a long time until the very end of the + # test when HAR was recorded but we'd abort it immediately. + if response.get("status") == -1: + return + body = response["body"] + assert body is not None + await route.fulfill( + status=response.get("status"), + headers={ + v["name"]: v["value"] + for v in cast(HeadersArray, response.get("headers", [])) + }, + body=base64.b64decode(body), + ) + return + + if action == "error": + pass + # Report the error, but fall through to the default handler. + + if self._not_found_action == "abort": + await route.abort() + return + + await route.fallback() + + async def add_context_route(self, context: "BrowserContext") -> None: + await context.route( + url=self._options_url_match or "**/*", + handler=lambda route, _: asyncio.create_task(self._handle(route)), + ) + + async def add_page_route(self, page: "Page") -> None: + await page.route( + url=self._options_url_match or "**/*", + handler=lambda route, _: asyncio.create_task(self._handle(route)), + ) + + def dispose(self) -> None: + self._local_utils._channel.send_may_fail( + "harClose", None, {"harId": self._har_id} + ) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_helper.py b/venv/lib/python3.11/site-packages/playwright/_impl/_helper.py new file mode 100644 index 0000000..4f97937 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_helper.py @@ -0,0 +1,616 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import datetime +import math +import os +import re +import time +import traceback +from pathlib import Path +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Dict, + List, + Literal, + Optional, + Pattern, + Set, + Tuple, + TypedDict, + TypeVar, + Union, + cast, + overload, +) +from urllib.parse import ParseResult, urljoin, urlparse, urlunparse + +from playwright._impl._api_structures import NameValue +from playwright._impl._errors import ( + Error, + TargetClosedError, + TimeoutError, + is_target_closed_error, + rewrite_error, +) +from playwright._impl._glob import glob_to_regex_pattern +from playwright._impl._greenlets import RouteGreenlet +from playwright._impl._str_utils import escape_regex_flags + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._internal_structures import HeadersArray + from playwright._impl._network import Request, Response, Route, WebSocketRoute + +URLMatch = Union[str, Pattern[str], Callable[[str], bool]] +URLMatchRequest = Union[ + str, Pattern[str], Callable[["Request"], Union[bool, Awaitable[bool]]] +] +URLMatchResponse = Union[ + str, Pattern[str], Callable[["Response"], Union[bool, Awaitable[bool]]] +] +RouteHandlerCallback = Union[ + Callable[["Route"], Any], Callable[["Route", "Request"], Any] +] +WebSocketRouteHandlerCallback = Callable[["WebSocketRoute"], Any] + +ColorScheme = Literal["dark", "light", "no-preference", "null"] +ForcedColors = Literal["active", "none", "null"] +Contrast = Literal["more", "no-preference", "null"] +ReducedMotion = Literal["no-preference", "null", "reduce"] +DocumentLoadState = Literal["commit", "domcontentloaded", "load", "networkidle"] +KeyboardModifier = Literal["Alt", "Control", "ControlOrMeta", "Meta", "Shift"] +MouseButton = Literal["left", "middle", "right"] +ServiceWorkersPolicy = Literal["allow", "block"] +HarMode = Literal["full", "minimal"] +HarContentPolicy = Literal["attach", "embed", "omit"] +RouteFromHarNotFoundPolicy = Literal["abort", "fallback"] + + +class ErrorPayload(TypedDict, total=False): + message: str + name: str + stack: str + value: Optional[Any] + + +class HarRecordingMetadata(TypedDict, total=False): + path: str + content: Optional[HarContentPolicy] + + +def prepare_record_har_options(params: Dict) -> Dict[str, Any]: + out_params: Dict[str, Any] = {"path": str(params["recordHarPath"])} + if "recordHarUrlFilter" in params: + opt = params["recordHarUrlFilter"] + if isinstance(opt, str): + out_params["urlGlob"] = opt + if isinstance(opt, Pattern): + out_params["urlRegexSource"] = opt.pattern + out_params["urlRegexFlags"] = escape_regex_flags(opt) + del params["recordHarUrlFilter"] + if "recordHarMode" in params: + out_params["mode"] = params["recordHarMode"] + del params["recordHarMode"] + + new_content_api = None + old_content_api = None + if "recordHarContent" in params: + new_content_api = params["recordHarContent"] + del params["recordHarContent"] + if "recordHarOmitContent" in params: + old_content_api = params["recordHarOmitContent"] + del params["recordHarOmitContent"] + content = new_content_api or ("omit" if old_content_api else None) + if content: + out_params["content"] = content + + return out_params + + +class ParsedMessageParams(TypedDict): + type: str + guid: str + initializer: Dict + + +class ParsedMessagePayload(TypedDict, total=False): + id: str + guid: str + method: str + params: ParsedMessageParams + result: Any + error: ErrorPayload + errorDetails: Any + log: List[str] + + +class Document(TypedDict): + request: Optional[Any] + + +class FrameNavigatedEvent(TypedDict): + url: str + name: str + newDocument: Optional[Document] + error: Optional[str] + + +Env = Dict[str, Union[str, float, bool]] + + +def url_matches( + base_url: Optional[str], + url_string: str, + match: Optional[URLMatch], + websocket_url: bool = None, +) -> bool: + if not match: + return True + if isinstance(match, str): + match = re.compile( + resolve_glob_to_regex_pattern(base_url, match, websocket_url) + ) + if isinstance(match, Pattern): + return bool(match.search(url_string)) + return match(url_string) + + +def resolve_glob_to_regex_pattern( + base_url: Optional[str], glob: str, websocket_url: bool = None +) -> str: + if websocket_url: + base_url = to_websocket_base_url(base_url) + glob = resolve_glob_base(base_url, glob) + return glob_to_regex_pattern(glob) + + +def to_websocket_base_url(base_url: Optional[str]) -> Optional[str]: + if base_url is not None and re.match(r"^https?://", base_url): + base_url = re.sub(r"^http", "ws", base_url) + return base_url + + +def resolve_glob_base(base_url: Optional[str], match: str) -> str: + if match[0] == "*": + return match + + token_map: Dict[str, str] = {} + + def map_token(original: str, replacement: str) -> str: + if len(original) == 0: + return "" + token_map[replacement] = original + return replacement + + # Escaped `\\?` behaves the same as `?` in our glob patterns. + match = match.replace(r"\\?", "?") + # Special case about: URLs as they are not relative to base_url + if ( + match.startswith("about:") + or match.startswith("data:") + or match.startswith("chrome:") + or match.startswith("edge:") + or match.startswith("file:") + ): + # about: and data: URLs are not relative to base_url, so we return them as is. + return match + # Glob symbols may be escaped in the URL and some of them such as ? affect resolution, + # so we replace them with safe components first. + processed_parts = [] + for index, token in enumerate(match.split("/")): + if token in (".", "..", ""): + processed_parts.append(token) + continue + # Handle special case of http*://, note that the new schema has to be + # a web schema so that slashes are properly inserted after domain. + if index == 0 and token.endswith(":"): + # Replace any pattern with http: + if "*" in token or "{" in token: + processed_parts.append(map_token(token, "http:")) + else: + # Preserve explicit schema as is as it may affect trailing slashes after domain. + processed_parts.append(token) + continue + question_index = token.find("?") + if question_index == -1: + processed_parts.append(map_token(token, f"$_{index}_$")) + else: + new_prefix = map_token(token[:question_index], f"$_{index}_$") + new_suffix = map_token(token[question_index:], f"?$_{index}_$") + processed_parts.append(new_prefix + new_suffix) + + relative_path = "/".join(processed_parts) + resolved, case_insensitive_part = resolve_base_url(base_url, relative_path) + + for token, original in token_map.items(): + normalize = case_insensitive_part and token in case_insensitive_part + resolved = resolved.replace( + token, original.lower() if normalize else original, 1 + ) + + return resolved + + +def resolve_base_url( + base_url: Optional[str], given_url: str +) -> Tuple[str, Optional[str]]: + try: + url = nodelike_urlparse( + urljoin(base_url if base_url is not None else "", given_url) + ) + resolved = urlunparse(url) + # Schema and domain are case-insensitive. + hostname_port = ( + url.hostname or "" + ) # can't use parsed.netloc because it includes userinfo (username:password) + if url.port: + hostname_port += f":{url.port}" + case_insensitive_prefix = f"{url.scheme}://{hostname_port}" + return resolved, case_insensitive_prefix + except Exception: + return given_url, None + + +def nodelike_urlparse(url: str) -> ParseResult: + parsed = urlparse(url, allow_fragments=True) + + # https://url.spec.whatwg.org/#special-scheme + is_special_url = parsed.scheme in ["http", "https", "ws", "wss", "ftp", "file"] + if is_special_url: + # special urls have a list path, list paths are serialized as follows: https://url.spec.whatwg.org/#url-path-serializer + # urllib diverges, so we patch it here + if parsed.path == "": + parsed = parsed._replace(path="/") + + return parsed + + +class HarLookupResult(TypedDict, total=False): + action: Literal["error", "redirect", "fulfill", "noentry"] + message: Optional[str] + redirectURL: Optional[str] + status: Optional[int] + headers: Optional["HeadersArray"] + body: Optional[str] + + +DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS = 30000 +DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS = 180000 +PLAYWRIGHT_MAX_DEADLINE = 2147483647 # 2^31-1 + + +class TimeoutSettings: + + @staticmethod + def launch_timeout(timeout: Optional[float] = None) -> float: + return ( + timeout + if timeout is not None + else DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS + ) + + def __init__(self, parent: Optional["TimeoutSettings"]) -> None: + self._parent = parent + self._default_timeout: Optional[float] = None + self._default_navigation_timeout: Optional[float] = None + + def set_default_timeout(self, timeout: Optional[float]) -> None: + self._default_timeout = timeout + + def timeout(self, timeout: float = None) -> float: + if timeout is not None: + return timeout + if self._default_timeout is not None: + return self._default_timeout + if self._parent: + return self._parent.timeout() + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS + + def set_default_navigation_timeout( + self, navigation_timeout: Optional[float] + ) -> None: + self._default_navigation_timeout = navigation_timeout + + def default_navigation_timeout(self) -> Optional[float]: + return self._default_navigation_timeout + + def default_timeout(self) -> Optional[float]: + return self._default_timeout + + def navigation_timeout(self, timeout: float = None) -> float: + if timeout is not None: + return timeout + if self._default_navigation_timeout is not None: + return self._default_navigation_timeout + if self._default_timeout is not None: + return self._default_timeout + if self._parent: + return self._parent.navigation_timeout() + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS + + +def serialize_error(ex: Exception, tb: Optional[TracebackType]) -> ErrorPayload: + return ErrorPayload( + message=str(ex), name="Error", stack="".join(traceback.format_tb(tb)) + ) + + +def parse_error(error: ErrorPayload, log: Optional[str] = None) -> Error: + base_error_class = Error + if error.get("name") == "TimeoutError": + base_error_class = TimeoutError + if error.get("name") == "TargetClosedError": + base_error_class = TargetClosedError + if not log: + log = "" + exc = base_error_class(patch_error_message(error["message"]) + log) + exc._name = error["name"] + exc._stack = error["stack"] + return exc + + +def patch_error_message(message: str) -> str: + match = re.match(r"(\w+)(: expected .*)", message) + if match: + message = to_snake_case(match.group(1)) + match.group(2) + message = message.replace( + "Pass { acceptDownloads: true }", "Pass 'accept_downloads=True'" + ) + return message + + +def locals_to_params(args: Dict) -> Dict: + copy = {} + for key in args: + if key == "self": + continue + if args[key] is not None: + copy[key] = ( + args[key] + if not isinstance(args[key], Dict) + else locals_to_params(args[key]) + ) + return copy + + +def monotonic_time() -> int: + return math.floor(time.monotonic() * 1000) + + +def create_task_and_ignore_exception( + loop: asyncio.AbstractEventLoop, coro: Awaitable[Any] +) -> None: + async def _ignore_exception() -> None: + try: + await coro + except Exception: + pass + + loop.create_task(_ignore_exception()) + + +class RouteHandlerInvocation: + complete: "asyncio.Future" + route: "Route" + + def __init__(self, complete: "asyncio.Future", route: "Route") -> None: + self.complete = complete + self.route = route + + +class RouteHandler: + def __init__( + self, + base_url: Optional[str], + url: URLMatch, + handler: RouteHandlerCallback, + is_sync: bool, + times: Optional[int] = None, + ): + self._base_url = base_url + self.url = url + self.handler = handler + self._times = times if times else math.inf + self._handled_count = 0 + self._is_sync = is_sync + self._ignore_exception = False + self._active_invocations: Set[RouteHandlerInvocation] = set() + + def matches(self, request_url: str) -> bool: + return url_matches(self._base_url, request_url, self.url) + + async def handle(self, route: "Route") -> bool: + handler_invocation = RouteHandlerInvocation( + asyncio.get_running_loop().create_future(), route + ) + self._active_invocations.add(handler_invocation) + try: + return await self._handle_internal(route) + except Exception as e: + # If the handler was stopped (without waiting for completion), we ignore all exceptions. + if self._ignore_exception: + return False + if is_target_closed_error(e): + # We are failing in the handler because the target has closed. + # Give user a hint! + optional_async_prefix = "await " if not self._is_sync else "" + raise rewrite_error( + e, + f"\"{str(e)}\" while running route callback.\nConsider awaiting `{optional_async_prefix}page.unroute_all(behavior='ignoreErrors')`\nbefore the end of the test to ignore remaining routes in flight.", + ) + raise e + finally: + handler_invocation.complete.set_result(None) + self._active_invocations.remove(handler_invocation) + + async def _handle_internal(self, route: "Route") -> bool: + handled_future = route._start_handling() + + self._handled_count += 1 + if self._is_sync: + handler_finished_future = route._loop.create_future() + + def _handler() -> None: + try: + self.handler(route, route.request) # type: ignore + handler_finished_future.set_result(None) + except Exception as e: + handler_finished_future.set_exception(e) + + # As with event handlers, each route handler is a potentially blocking context + # so it needs a fiber. + g = RouteGreenlet(_handler) + g.switch() + await handler_finished_future + else: + coro_or_future = self.handler(route, route.request) # type: ignore + if coro_or_future: + # separate task so that we get a proper stack trace for exceptions / tracing api_name extraction + await asyncio.ensure_future(coro_or_future) + return await handled_future + + async def stop(self, behavior: Literal["ignoreErrors", "wait"]) -> None: + # When a handler is manually unrouted or its page/context is closed we either + # - wait for the current handler invocations to finish + # - or do not wait, if the user opted out of it, but swallow all exceptions + # that happen after the unroute/close. + if behavior == "ignoreErrors": + self._ignore_exception = True + else: + tasks = [] + for activation in self._active_invocations: + if not activation.route._did_throw: + tasks.append(activation.complete) + await asyncio.gather(*tasks) + + @property + def will_expire(self) -> bool: + return self._handled_count + 1 >= self._times + + @staticmethod + def prepare_interception_patterns( + handlers: List["RouteHandler"], + ) -> List[Dict[str, str]]: + patterns = [] + all = False + for handler in handlers: + if isinstance(handler.url, str): + patterns.append({"glob": handler.url}) + elif isinstance(handler.url, re.Pattern): + patterns.append( + { + "regexSource": handler.url.pattern, + "regexFlags": escape_regex_flags(handler.url), + } + ) + else: + all = True + if all: + return [{"glob": "**/*"}] + return patterns + + +to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") + + +def to_snake_case(name: str) -> str: + return to_snake_case_regex.sub(r"_\1", name).lower() + + +def make_dirs_for_file(path: Union[Path, str]) -> None: + if not os.path.isabs(path): + path = Path.cwd() / path + os.makedirs(os.path.dirname(path), exist_ok=True) + + +async def async_writefile(file: Union[str, Path], data: Union[str, bytes]) -> None: + def inner() -> None: + with open(file, "w" if isinstance(data, str) else "wb") as fh: + fh.write(data) + + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, inner) + + +async def async_readfile(file: Union[str, Path]) -> bytes: + def inner() -> bytes: + with open(file, "rb") as fh: + return fh.read() + + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, inner) + + +T = TypeVar("T") + + +def to_impl(obj: T) -> T: + if hasattr(obj, "_impl_obj"): + return cast(Any, obj)._impl_obj + return obj + + +def object_to_array(obj: Optional[Dict]) -> Optional[List[NameValue]]: + if not obj: + return None + result = [] + for key, value in obj.items(): + result.append(NameValue(name=key, value=str(value))) + return result + + +def is_file_payload(value: Optional[Any]) -> bool: + return ( + isinstance(value, dict) + and "name" in value + and "mimeType" in value + and "buffer" in value + ) + + +TEXTUAL_MIME_TYPE = re.compile( + r"^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$" +) + + +def is_textual_mime_type(mime_type: str) -> bool: + return bool(TEXTUAL_MIME_TYPE.match(mime_type)) + + +@overload +def to_milliseconds(value: Union[float, datetime.timedelta]) -> float: ... + + +@overload +def to_milliseconds(value: None) -> None: ... + + +@overload +def to_milliseconds( + value: Union[float, datetime.timedelta, None], +) -> Optional[float]: ... + + +def to_milliseconds( + value: Union[float, datetime.timedelta, None], +) -> Optional[float]: + if value is None: + return None + if isinstance(value, datetime.timedelta): + return value / datetime.timedelta(milliseconds=1) + return value diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_impl_to_api_mapping.py b/venv/lib/python3.11/site-packages/playwright/_impl/_impl_to_api_mapping.py new file mode 100644 index 0000000..3aa0151 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_impl_to_api_mapping.py @@ -0,0 +1,159 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from typing import Any, Callable, Dict, List, Optional, Sequence, Union + +from playwright._impl._errors import Error +from playwright._impl._map import Map +from playwright._impl._signature import signature + +API_ATTR = "_pw_api_instance_" +IMPL_ATTR = "_pw_impl_instance_" + + +class ImplWrapper: + def __init__(self, impl_obj: Any) -> None: + self._impl_obj = impl_obj + + def __repr__(self) -> str: + return self._impl_obj.__repr__() + + +class ImplToApiMapping: + def __init__(self) -> None: + self._mapping: Dict[type, type] = {} + + def register(self, impl_class: type, api_class: type) -> None: + self._mapping[impl_class] = api_class + + def from_maybe_impl( + self, obj: Any, visited: Optional[Map[Any, Union[List, Dict]]] = None + ) -> Any: + # Python does share default arguments between calls, so we need to + # create a new map if it is not provided. + if not visited: + visited = Map() + if not obj: + return obj + if isinstance(obj, dict): + if obj in visited: + return visited[obj] + o: Dict = {} + visited[obj] = o + for name, value in obj.items(): + o[name] = self.from_maybe_impl(value, visited) + return o + if isinstance(obj, list): + if obj in visited: + return visited[obj] + a: List = [] + visited[obj] = a + for item in obj: + a.append(self.from_maybe_impl(item, visited)) + return a + api_class = self._mapping.get(type(obj)) + if api_class: + api_instance = getattr(obj, API_ATTR, None) + if not api_instance: + api_instance = api_class(obj) + setattr(obj, API_ATTR, api_instance) + return api_instance + else: + return obj + + def from_impl(self, obj: Any) -> Any: + assert obj + result = self.from_maybe_impl(obj) + assert result + return result + + def from_impl_nullable(self, obj: Any = None) -> Optional[Any]: + return self.from_impl(obj) if obj else None + + def from_impl_list(self, items: Sequence[Any]) -> List[Any]: + return list(map(lambda a: self.from_impl(a), items)) + + def from_impl_dict(self, map: Dict[str, Any]) -> Dict[str, Any]: + return {name: self.from_impl(value) for name, value in map.items()} + + def to_impl( + self, obj: Any, visited: Optional[Map[Any, Union[List, Dict]]] = None + ) -> Any: + if visited is None: + visited = Map() + try: + if not obj: + return obj + if isinstance(obj, dict): + if obj in visited: + return visited[obj] + o: Dict = {} + visited[obj] = o + for name, value in obj.items(): + o[name] = self.to_impl(value, visited) + return o + if isinstance(obj, list): + if obj in visited: + return visited[obj] + a: List = [] + visited[obj] = a + for item in obj: + a.append(self.to_impl(item, visited)) + return a + if isinstance(obj, ImplWrapper): + return obj._impl_obj + return obj + except RecursionError: + raise Error("Maximum argument depth exceeded") + + def wrap_handler(self, handler: Callable[..., Any]) -> Callable[..., None]: + def wrapper_func(*args: Any) -> Any: + parameters = signature(handler).parameters + has_varargs = any( + parameter.kind == inspect.Parameter.VAR_POSITIONAL + for parameter in parameters.values() + ) + arg_count = ( + len(args) + if has_varargs + else sum( + parameter.kind + in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) + for parameter in parameters.values() + ) + ) + return handler( + *list(map(lambda a: self.from_maybe_impl(a), args))[:arg_count] + ) + + if inspect.ismethod(handler): + wrapper = getattr(handler.__self__, IMPL_ATTR + handler.__name__, None) + if not wrapper: + wrapper = wrapper_func + setattr( + handler.__self__, + IMPL_ATTR + handler.__name__, + wrapper, + ) + return wrapper + + wrapper = getattr(handler, IMPL_ATTR, None) + if not wrapper: + wrapper = wrapper_func + setattr(handler, IMPL_ATTR, wrapper) + return wrapper diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_input.py b/venv/lib/python3.11/site-packages/playwright/_impl/_input.py new file mode 100644 index 0000000..8a39242 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_input.py @@ -0,0 +1,111 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from playwright._impl._connection import Channel +from playwright._impl._helper import MouseButton, locals_to_params + + +class Keyboard: + def __init__(self, channel: Channel) -> None: + self._channel = channel + self._loop = channel._connection._loop + self._dispatcher_fiber = channel._connection._dispatcher_fiber + + async def down(self, key: str) -> None: + await self._channel.send("keyboardDown", None, locals_to_params(locals())) + + async def up(self, key: str) -> None: + await self._channel.send("keyboardUp", None, locals_to_params(locals())) + + async def insert_text(self, text: str) -> None: + await self._channel.send("keyboardInsertText", None, locals_to_params(locals())) + + async def type(self, text: str, delay: float = None) -> None: + await self._channel.send("keyboardType", None, locals_to_params(locals())) + + async def press(self, key: str, delay: float = None) -> None: + await self._channel.send("keyboardPress", None, locals_to_params(locals())) + + +class Mouse: + def __init__(self, channel: Channel) -> None: + self._channel = channel + self._loop = channel._connection._loop + self._dispatcher_fiber = channel._connection._dispatcher_fiber + + async def move(self, x: float, y: float, steps: int = None) -> None: + await self._channel.send("mouseMove", None, locals_to_params(locals())) + + async def down( + self, + button: MouseButton = None, + clickCount: int = None, + ) -> None: + await self._channel.send("mouseDown", None, locals_to_params(locals())) + + async def up( + self, + button: MouseButton = None, + clickCount: int = None, + ) -> None: + await self._channel.send("mouseUp", None, locals_to_params(locals())) + + async def _click( + self, + x: float, + y: float, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + title: str = None, + ) -> None: + await self._channel.send( + "mouseClick", None, locals_to_params(locals()), title=title + ) + + async def click( + self, + x: float, + y: float, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + ) -> None: + params = locals() + del params["self"] + await self._click(**params) + + async def dblclick( + self, + x: float, + y: float, + delay: float = None, + button: MouseButton = None, + ) -> None: + await self._click( + x, y, delay=delay, button=button, clickCount=2, title="Double click" + ) + + async def wheel(self, deltaX: float, deltaY: float) -> None: + await self._channel.send("mouseWheel", None, locals_to_params(locals())) + + +class Touchscreen: + def __init__(self, channel: Channel) -> None: + self._channel = channel + self._loop = channel._connection._loop + self._dispatcher_fiber = channel._connection._dispatcher_fiber + + async def tap(self, x: float, y: float) -> None: + await self._channel.send("touchscreenTap", None, locals_to_params(locals())) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_internal_structures.py b/venv/lib/python3.11/site-packages/playwright/_impl/_internal_structures.py new file mode 100644 index 0000000..1b34754 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_internal_structures.py @@ -0,0 +1,149 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Types that the implementation uses internally. The public TypedDicts are +# generated into _api_structures.py. + +from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict + +from playwright._impl._api_structures import NameValue + +HeadersArray = List[NameValue] +Headers = Dict[str, str] + + +class ServerFilePayload(TypedDict): + name: str + mimeType: str + buffer: str + + +class FormField(TypedDict, total=False): + name: str + value: Optional[str] + file: Optional[ServerFilePayload] + + +class ExpectedTextValue(TypedDict, total=False): + string: str + regexSource: str + regexFlags: str + matchSubstring: bool + normalizeWhiteSpace: bool + ignoreCase: Optional[bool] + + +class FrameExpectOptions(TypedDict, total=False): + expressionArg: Any + expectedText: Optional[Sequence[ExpectedTextValue]] + expectedNumber: Optional[float] + expectedValue: Optional[Any] + useInnerText: Optional[bool] + isNot: bool + timeout: Optional[float] + pseudo: Optional[str] + + +class FrameExpectResult(TypedDict, total=False): + matches: bool + received: Any + log: Optional[List[str]] + errorMessage: Optional[str] + timedOut: Optional[bool] + + +AriaRole = Literal[ + "alert", + "alertdialog", + "application", + "article", + "banner", + "blockquote", + "button", + "caption", + "cell", + "checkbox", + "code", + "columnheader", + "combobox", + "complementary", + "contentinfo", + "definition", + "deletion", + "dialog", + "directory", + "document", + "emphasis", + "feed", + "figure", + "form", + "generic", + "grid", + "gridcell", + "group", + "heading", + "img", + "insertion", + "link", + "list", + "listbox", + "listitem", + "log", + "main", + "marquee", + "math", + "menu", + "menubar", + "menuitem", + "menuitemcheckbox", + "menuitemradio", + "meter", + "navigation", + "none", + "note", + "option", + "paragraph", + "presentation", + "progressbar", + "radio", + "radiogroup", + "region", + "row", + "rowgroup", + "rowheader", + "scrollbar", + "search", + "searchbox", + "separator", + "slider", + "spinbutton", + "status", + "strong", + "subscript", + "superscript", + "switch", + "tab", + "table", + "tablist", + "tabpanel", + "term", + "textbox", + "time", + "timer", + "toolbar", + "tooltip", + "tree", + "treegrid", + "treeitem", +] diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_js_handle.py b/venv/lib/python3.11/site-packages/playwright/_impl/_js_handle.py new file mode 100644 index 0000000..49a32ee --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_js_handle.py @@ -0,0 +1,349 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import collections.abc +import datetime +import math +import re +import struct +import traceback +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Pattern, Union +from urllib.parse import ParseResult, urlparse, urlunparse + +from playwright._impl._connection import Channel, ChannelOwner, from_channel +from playwright._impl._errors import Error, is_target_closed_error +from playwright._impl._map import Map +from playwright._impl._str_utils import escape_regex_flags, parse_regex_flags + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._element_handle import ElementHandle + + +Serializable = Any + + +class VisitorInfo: + visited: Map[Any, int] + last_id: int + + def __init__(self) -> None: + self.visited = Map() + self.last_id = 0 + + def visit(self, obj: Any) -> int: + assert obj not in self.visited + self.last_id += 1 + self.visited[obj] = self.last_id + return self.last_id + + +class JSHandle(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._preview = self._initializer["preview"] + self._channel.on( + "previewUpdated", lambda params: self._on_preview_updated(params["preview"]) + ) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self._preview + + def _on_preview_updated(self, preview: str) -> None: + self._preview = preview + + async def evaluate(self, expression: str, arg: Serializable = None) -> Any: + return parse_result( + await self._channel.send( + "evaluateExpression", + None, + dict( + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def evaluate_handle( + self, expression: str, arg: Serializable = None + ) -> "JSHandle": + return from_channel( + await self._channel.send( + "evaluateExpressionHandle", + None, + dict( + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def get_property(self, propertyName: str) -> "JSHandle": + return from_channel( + await self._channel.send("getProperty", None, dict(name=propertyName)) + ) + + async def get_properties(self) -> Dict[str, "JSHandle"]: + return { + prop["name"]: from_channel(prop["value"]) + for prop in await self._channel.send( + "getPropertyList", + None, + ) + } + + def as_element(self) -> Optional["ElementHandle"]: + return None + + async def dispose(self) -> None: + try: + await self._channel.send( + "dispose", + None, + ) + except Exception as e: + if not is_target_closed_error(e): + raise e + + async def json_value(self) -> Any: + return parse_result( + await self._channel.send( + "jsonValue", + None, + ) + ) + + +def serialize_value( + value: Any, handles: List[Channel], visitor_info: Optional[VisitorInfo] = None +) -> Any: + if visitor_info is None: + visitor_info = VisitorInfo() + if isinstance(value, JSHandle): + h = len(handles) + handles.append(value._channel) + return dict(h=h) + if value is None: + return dict(v="null") + if isinstance(value, float): + if value == float("inf"): + return dict(v="Infinity") + if value == float("-inf"): + return dict(v="-Infinity") + if value == 0 and math.copysign(1.0, value) == -1.0: + return dict(v="-0") + if math.isnan(value): + return dict(v="NaN") + if isinstance(value, datetime.datetime): + # Node.js Date objects are always in UTC. + return { + "d": datetime.datetime.strftime( + value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ" + ) + } + if isinstance(value, Exception): + return { + "e": { + "m": str(value), + "n": ( + (value.name or "") + if isinstance(value, Error) + else value.__class__.__name__ + ), + "s": ( + (value.stack or "") + if isinstance(value, Error) + else "".join( + traceback.format_exception(type(value), value=value, tb=None) + ) + ), + } + } + if isinstance(value, bool): + return {"b": value} + if isinstance(value, (int, float)): + return {"n": value} + if isinstance(value, str): + return {"s": value} + if isinstance(value, ParseResult): + return {"u": urlunparse(value)} + if isinstance(value, Pattern): + if not isinstance(value.pattern, str): + raise Error("Only str patterns are supported.") + return {"r": {"p": value.pattern, "f": escape_regex_flags(value)}} + + if value in visitor_info.visited: + return dict(ref=visitor_info.visited[value]) + + if isinstance(value, collections.abc.Sequence) and not isinstance(value, str): + id = visitor_info.visit(value) + a = [] + for e in value: + a.append(serialize_value(e, handles, visitor_info)) + return dict(a=a, id=id) + + if isinstance(value, dict): + id = visitor_info.visit(value) + o = [] + for name in value: + o.append( + {"k": name, "v": serialize_value(value[name], handles, visitor_info)} + ) + return dict(o=o, id=id) + return dict(v="undefined") + + +def serialize_argument(arg: Serializable = None) -> Any: + handles: List[Channel] = [] + value = serialize_value(arg, handles) + return dict(value=value, handles=handles) + + +def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: + if refs is None: + refs = {} + if value is None: + return None + if isinstance(value, dict): + if "ref" in value: + return refs[value["ref"]] + + if "v" in value: + v = value["v"] + if v == "Infinity": + return float("inf") + if v == "-Infinity": + return float("-inf") + if v == "-0": + return float("-0") + if v == "NaN": + return float("nan") + if v == "undefined": + return None + if v == "null": + return None + return v + + if "u" in value: + return urlparse(value["u"]) + + if "bi" in value: + return int(value["bi"]) + + if "e" in value: + error = Error(value["e"]["m"]) + error._name = value["e"]["n"] + error._stack = value["e"]["s"] + return error + + if "r" in value: + try: + return re.compile(value["r"]["p"], parse_regex_flags(value["r"]["f"])) + except re.error: + # JavaScript-only syntax, e.g. /(?x)/, has no re equivalent. + return value + + if "a" in value: + a: List = [] + refs[value["id"]] = a + for e in value["a"]: + a.append(parse_value(e, refs)) + return a + + if "d" in value: + # Node.js Date objects are always in UTC. + return datetime.datetime.strptime( + value["d"], "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=datetime.timezone.utc) + + if "o" in value: + o: Dict = {} + refs[value["id"]] = o + for e in value["o"]: + o[e["k"]] = parse_value(e["v"], refs) + return o + + if "n" in value: + return value["n"] + + if "s" in value: + return value["s"] + + if "b" in value: + return value["b"] + + if "ta" in value: + encoded_bytes = value["ta"]["b"] + decoded_bytes = base64.b64decode(encoded_bytes) + array_type = value["ta"]["k"] + if array_type == "i8": + word_size = 1 + fmt = "b" + elif array_type == "ui8" or array_type == "ui8c": + word_size = 1 + fmt = "B" + elif array_type == "i16": + word_size = 2 + fmt = "h" + elif array_type == "ui16": + word_size = 2 + fmt = "H" + elif array_type == "i32": + word_size = 4 + fmt = "i" + elif array_type == "ui32": + word_size = 4 + fmt = "I" + elif array_type == "f32": + word_size = 4 + fmt = "f" + elif array_type == "f64": + word_size = 8 + fmt = "d" + elif array_type == "bi64": + word_size = 8 + fmt = "q" + elif array_type == "bui64": + word_size = 8 + fmt = "Q" + else: + raise ValueError(f"Unsupported array type: {array_type}") + + byte_len = len(decoded_bytes) + if byte_len % word_size != 0: + raise ValueError( + f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}" + ) + + if byte_len == 0: + return [] + array_len = byte_len // word_size + # "<" denotes little-endian + format_string = f"<{array_len}{fmt}" + return list(struct.unpack(format_string, decoded_bytes)) + return value + + +def parse_result(result: Any) -> Any: + return parse_value(result) + + +def add_source_url_to_script(source: str, path: Union[str, Path]) -> str: + return source + "\n//# sourceURL=" + str(path).replace("\n", "") diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_json_pipe.py b/venv/lib/python3.11/site-packages/playwright/_impl/_json_pipe.py new file mode 100644 index 0000000..41973b8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_json_pipe.py @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from typing import Dict, Optional, cast + +from pyee.asyncio import AsyncIOEventEmitter + +from playwright._impl._connection import Channel +from playwright._impl._errors import TargetClosedError +from playwright._impl._helper import Error, ParsedMessagePayload +from playwright._impl._transport import Transport + + +class JsonPipeTransport(AsyncIOEventEmitter, Transport): + def __init__( + self, + loop: asyncio.AbstractEventLoop, + pipe_channel: Channel, + ) -> None: + super().__init__(loop) + Transport.__init__(self, loop) + self._stop_requested = False + self._pipe_channel = pipe_channel + + def request_stop(self) -> None: + self._stop_requested = True + self._pipe_channel.send_no_reply("close", None, {}) + + def dispose(self) -> None: + self.on_error_future.cancel() + self._stopped_future.cancel() + + async def wait_until_stopped(self) -> None: + await self._stopped_future + + async def connect(self) -> None: + self._stopped_future: asyncio.Future = asyncio.Future() + + def handle_message(message: Dict) -> None: + if self._stop_requested: + return + self.on_message(cast(ParsedMessagePayload, message)) + + def handle_closed(reason: Optional[str]) -> None: + self.emit("close", reason) + if reason: + self.on_error_future.set_exception(TargetClosedError(reason)) + self._stopped_future.set_result(None) + + self._pipe_channel.on( + "message", + lambda params: handle_message(params["message"]), + ) + self._pipe_channel.on( + "closed", + lambda params: handle_closed(params.get("reason")), + ) + + async def run(self) -> None: + await self._stopped_future + + def send(self, message: Dict) -> None: + if self._stop_requested: + raise Error("Playwright connection closed") + self._pipe_channel.send_no_reply("send", None, {"message": message}) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_local_utils.py b/venv/lib/python3.11/site-packages/playwright/_impl/_local_utils.py new file mode 100644 index 0000000..7813c52 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_local_utils.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +from typing import Dict, List, Optional, cast + +from playwright._impl._connection import ChannelOwner, StackFrame +from playwright._impl._helper import HarLookupResult, locals_to_params +from playwright._impl._internal_structures import HeadersArray + + +class LocalUtils(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self.devices = { + device["name"]: parse_device_descriptor(device["descriptor"]) + for device in initializer["deviceDescriptors"] + } + + async def zip(self, params: Dict) -> None: + await self._channel.send("zip", None, params) + + async def har_open(self, file: str) -> None: + params = locals_to_params(locals()) + await self._channel.send("harOpen", None, params) + + async def har_lookup( + self, + harId: str, + url: str, + method: str, + headers: HeadersArray, + isNavigationRequest: bool, + postData: Optional[bytes] = None, + ) -> HarLookupResult: + params = locals_to_params(locals()) + if "postData" in params: + params["postData"] = base64.b64encode(params["postData"]).decode() + return cast( + HarLookupResult, + await self._channel.send_return_as_dict("harLookup", None, params), + ) + + async def har_close(self, harId: str) -> None: + params = locals_to_params(locals()) + await self._channel.send("harClose", None, params) + + async def har_unzip(self, zipFile: str, harFile: str) -> None: + params = locals_to_params(locals()) + await self._channel.send("harUnzip", None, params) + + async def tracing_started( + self, tracesDir: Optional[str], traceName: str, live: bool = False + ) -> str: + params = locals_to_params(locals()) + return await self._channel.send("tracingStarted", None, params) + + async def trace_discarded(self, stacks_id: str) -> None: + return await self._channel.send("traceDiscarded", None, {"stacksId": stacks_id}) + + def add_stack_to_tracing_no_reply(self, id: str, frames: List[StackFrame]) -> None: + self._channel.send_no_reply( + "addStackToTracingNoReply", + None, + { + "callData": { + "stack": frames, + "id": id, + } + }, + ) + + +def parse_device_descriptor(dict: Dict) -> Dict: + return { + "user_agent": dict["userAgent"], + "viewport": dict["viewport"], + "device_scale_factor": dict["deviceScaleFactor"], + "is_mobile": dict["isMobile"], + "has_touch": dict["hasTouch"], + "default_browser_type": dict["defaultBrowserType"], + } diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_locator.py b/venv/lib/python3.11/site-packages/playwright/_impl/_locator.py new file mode 100644 index 0000000..5e3df1b --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_locator.py @@ -0,0 +1,1053 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import pathlib +import re +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Dict, + List, + Literal, + Optional, + Pattern, + Sequence, + Tuple, + TypeVar, + Union, +) + +from playwright._impl._api_structures import ( + DropPayload, + FilePayload, + FloatRect, + Position, +) +from playwright._impl._element_handle import ElementHandle +from playwright._impl._helper import ( + Error, + KeyboardModifier, + MouseButton, + locals_to_params, + monotonic_time, + to_impl, +) +from playwright._impl._internal_structures import ( + AriaRole, + FrameExpectOptions, + FrameExpectResult, +) +from playwright._impl._js_handle import Serializable, serialize_argument +from playwright._impl._str_utils import ( + escape_for_attribute_selector, + escape_for_text_selector, +) + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._frame import Frame + from playwright._impl._js_handle import JSHandle + from playwright._impl._page import Page + +T = TypeVar("T") + + +class Locator: + def __init__( + self, + frame: "Frame", + selector: str, + has_text: Union[str, Pattern[str]] = None, + has_not_text: Union[str, Pattern[str]] = None, + has: "Locator" = None, + has_not: "Locator" = None, + visible: bool = None, + ) -> None: + self._frame = frame + self._selector = selector + self._loop = frame._loop + self._dispatcher_fiber = frame._connection._dispatcher_fiber + # Python code for this locator, known only for normalized locators. + self._locator_code: Optional[str] = None + + if has_text: + self._selector += f" >> internal:has-text={escape_for_text_selector(has_text, exact=False)}" + + if has: + if has._frame != frame: + raise Error('Inner "has" locator must belong to the same frame.') + self._selector += " >> internal:has=" + json.dumps( + has._selector, ensure_ascii=False + ) + + if has_not_text: + self._selector += f" >> internal:has-not-text={escape_for_text_selector(has_not_text, exact=False)}" + + if has_not: + locator = has_not + if locator._frame != frame: + raise Error('Inner "has_not" locator must belong to the same frame.') + self._selector += " >> internal:has-not=" + json.dumps(locator._selector) + + if visible is not None: + self._selector += f" >> visible={bool_to_js_bool(visible)}" + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + if self._locator_code is not None: + return self._locator_code + return self.__repr__() + + async def _with_element( + self, + task: Callable[[ElementHandle, float], Awaitable[T]], + timeout: float = None, + ) -> T: + timeout = self._frame._timeout(timeout) + deadline = (monotonic_time() + timeout) if timeout else 0 + handle = await self.element_handle(timeout=timeout) + if not handle: + raise Error(f"Could not resolve {self._selector} to DOM Element") + try: + return await task( + handle, + (deadline - monotonic_time()) if deadline else 0, + ) + finally: + await handle.dispose() + + def _equals(self, locator: "Locator") -> bool: + return self._frame == locator._frame and self._selector == locator._selector + + @property + def page(self) -> "Page": + return self._frame.page + + async def bounding_box(self, timeout: float = None) -> Optional[FloatRect]: + return await self._with_element( + lambda h, _: h.bounding_box(), + timeout, + ) + + async def check( + self, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.check(self._selector, strict=True, **params) + + async def click( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame._click(self._selector, strict=True, **params) + + async def dblclick( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.dblclick(self._selector, strict=True, **params) + + async def dispatch_event( + self, + type: str, + eventInit: Dict = None, + timeout: float = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.dispatch_event(self._selector, strict=True, **params) + + async def evaluate( + self, expression: str, arg: Serializable = None, timeout: float = None + ) -> Any: + return await self._with_element( + lambda h, _: h.evaluate(expression, arg), + timeout, + ) + + async def evaluate_all(self, expression: str, arg: Serializable = None) -> Any: + params = locals_to_params(locals()) + return await self._frame.eval_on_selector_all(self._selector, **params) + + async def evaluate_handle( + self, expression: str, arg: Serializable = None, timeout: float = None + ) -> "JSHandle": + return await self._with_element( + lambda h, _: h.evaluate_handle(expression, arg), timeout + ) + + async def fill( + self, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.fill(self._selector, strict=True, **params) + + async def clear( + self, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + ) -> None: + params = locals_to_params(locals()) + await self._frame._fill(self._selector, value="", title="Clear", **params) + + def locator( + self, + selectorOrLocator: Union[str, "Locator"], + hasText: Union[str, Pattern[str]] = None, + hasNotText: Union[str, Pattern[str]] = None, + has: "Locator" = None, + hasNot: "Locator" = None, + ) -> "Locator": + if isinstance(selectorOrLocator, str): + return Locator( + self._frame, + f"{self._selector} >> {selectorOrLocator}", + has_text=hasText, + has_not_text=hasNotText, + has_not=hasNot, + has=has, + ) + selectorOrLocator = to_impl(selectorOrLocator) + if selectorOrLocator._frame != self._frame: + raise Error("Locators must belong to the same frame.") + return Locator( + self._frame, + f"{self._selector} >> internal:chain={json.dumps(selectorOrLocator._selector)}", + has_text=hasText, + has_not_text=hasNotText, + has_not=hasNot, + has=has, + ) + + def get_by_alt_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_alt_text_selector(text, exact=exact)) + + def get_by_label( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_label_selector(text, exact=exact)) + + def get_by_placeholder( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_placeholder_selector(text, exact=exact)) + + def get_by_role( + self, + role: AriaRole, + checked: bool = None, + disabled: bool = None, + expanded: bool = None, + includeHidden: bool = None, + level: int = None, + name: Union[str, Pattern[str]] = None, + pressed: bool = None, + selected: bool = None, + exact: bool = None, + description: Union[str, Pattern[str]] = None, + ) -> "Locator": + return self.locator( + get_by_role_selector( + role, + checked=checked, + disabled=disabled, + expanded=expanded, + includeHidden=includeHidden, + level=level, + name=name, + pressed=pressed, + selected=selected, + exact=exact, + description=description, + ) + ) + + def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": + return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId)) + + def get_by_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_text_selector(text, exact=exact)) + + def get_by_title( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_title_selector(text, exact=exact)) + + def frame_locator(self, selector: str) -> "FrameLocator": + return FrameLocator(self._frame, self._selector + " >> " + selector) + + async def element_handle( + self, + timeout: float = None, + ) -> ElementHandle: + params = locals_to_params(locals()) + handle = await self._frame.wait_for_selector( + self._selector, strict=True, state="attached", **params + ) + assert handle + return handle + + async def element_handles(self) -> List[ElementHandle]: + return await self._frame.query_selector_all(self._selector) + + @property + def first(self) -> "Locator": + return Locator(self._frame, f"{self._selector} >> nth=0") + + @property + def last(self) -> "Locator": + return Locator(self._frame, f"{self._selector} >> nth=-1") + + def nth(self, index: int) -> "Locator": + return Locator(self._frame, f"{self._selector} >> nth={index}") + + @property + def content_frame(self) -> "FrameLocator": + return FrameLocator(self._frame, self._selector) + + def describe(self, description: str) -> "Locator": + return Locator( + self._frame, + f"{self._selector} >> internal:describe={json.dumps(description)}", + ) + + @property + def description(self) -> Optional[str]: + try: + match = re.search( + r' >> internal:describe=("(?:[^"\\]|\\.)*")$', self._selector + ) + if match: + description = json.loads(match.group(1)) + if isinstance(description, str): + return description + except (json.JSONDecodeError, ValueError): + pass + return None + + def filter( + self, + hasText: Union[str, Pattern[str]] = None, + hasNotText: Union[str, Pattern[str]] = None, + has: "Locator" = None, + hasNot: "Locator" = None, + visible: bool = None, + ) -> "Locator": + return Locator( + self._frame, + self._selector, + has_text=hasText, + has_not_text=hasNotText, + has=has, + has_not=hasNot, + visible=visible, + ) + + @property + def visible(self) -> "Locator": + return Locator(self._frame, self._selector, visible=True) + + def or_(self, locator: "Locator") -> "Locator": + if locator._frame != self._frame: + raise Error("Locators must belong to the same frame.") + return Locator( + self._frame, + self._selector + " >> internal:or=" + json.dumps(locator._selector), + ) + + def and_(self, locator: "Locator") -> "Locator": + if locator._frame != self._frame: + raise Error("Locators must belong to the same frame.") + return Locator( + self._frame, + self._selector + " >> internal:and=" + json.dumps(locator._selector), + ) + + def within(self, locator: "Locator") -> "Locator": + return locator.locator(self) + + async def focus(self, timeout: float = None) -> None: + params = locals_to_params(locals()) + return await self._frame.focus(self._selector, strict=True, **params) + + async def blur(self, timeout: float = None) -> None: + await self._frame._channel.send( + "blur", + self._frame._timeout, + { + "selector": self._selector, + "strict": True, + **locals_to_params(locals()), + }, + ) + + async def all( + self, + ) -> List["Locator"]: + result = [] + for index in range(await self.count()): + result.append(self.nth(index)) + return result + + async def count( + self, + ) -> int: + return await self._frame._query_count(self._selector) + + async def drag_to( + self, + target: "Locator", + force: bool = None, + noWaitAfter: bool = None, + timeout: float = None, + trial: bool = None, + sourcePosition: Position = None, + targetPosition: Position = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + params = locals_to_params(locals()) + del params["target"] + return await self._frame.drag_and_drop( + self._selector, target._selector, strict=True, **params + ) + + async def drop( + self, + payload: DropPayload, + position: Position = None, + timeout: float = None, + ) -> None: + await self._frame._drop( + self._selector, + payload, + strict=True, + position=position, + timeout=timeout, + ) + + async def get_attribute(self, name: str, timeout: float = None) -> Optional[str]: + params = locals_to_params(locals()) + return await self._frame.get_attribute( + self._selector, + strict=True, + **params, + ) + + async def hover( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.hover( + self._selector, + strict=True, + **params, + ) + + async def inner_html(self, timeout: float = None) -> str: + params = locals_to_params(locals()) + return await self._frame.inner_html( + self._selector, + strict=True, + **params, + ) + + async def inner_text(self, timeout: float = None) -> str: + params = locals_to_params(locals()) + return await self._frame.inner_text( + self._selector, + strict=True, + **params, + ) + + async def input_value(self, timeout: float = None) -> str: + params = locals_to_params(locals()) + return await self._frame.input_value( + self._selector, + strict=True, + **params, + ) + + async def is_checked(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_checked( + self._selector, + strict=True, + **params, + ) + + async def is_disabled(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_disabled( + self._selector, + strict=True, + **params, + ) + + async def is_editable(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_editable( + self._selector, + strict=True, + **params, + ) + + async def is_enabled(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_enabled( + self._selector, + strict=True, + **params, + ) + + async def is_hidden(self, timeout: float = None) -> bool: + # timeout is deprecated and does nothing + return await self._frame.is_hidden( + self._selector, + strict=True, + ) + + async def is_visible(self, timeout: float = None) -> bool: + # timeout is deprecated and does nothing + return await self._frame.is_visible( + self._selector, + strict=True, + ) + + async def press( + self, + key: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.press(self._selector, strict=True, **params) + + async def screenshot( + self, + timeout: float = None, + type: Literal["jpeg", "png", "webp"] = None, + path: Union[str, pathlib.Path] = None, + quality: int = None, + omitBackground: bool = None, + animations: Literal["allow", "disabled"] = None, + caret: Literal["hide", "initial"] = None, + scale: Literal["css", "device"] = None, + mask: Sequence["Locator"] = None, + maskColor: str = None, + style: str = None, + ) -> bytes: + params = locals_to_params(locals()) + return await self._with_element( + lambda h, timeout: h.screenshot( + **{**params, "timeout": timeout}, + ), + timeout, + ) + + async def aria_snapshot( + self, + timeout: float = None, + depth: int = None, + mode: Literal["ai", "default"] = None, + boxes: bool = None, + ) -> str: + return await self._frame._channel.send( + "ariaSnapshot", + self._frame._timeout, + { + "selector": self._selector, + **locals_to_params(locals()), + }, + ) + + async def normalize(self) -> "Locator": + result = await self._frame._channel.send_return_as_dict( + "resolveSelector", + None, + {"selector": self._selector, "sdkLanguage": "python"}, + ) + locator = Locator(self._frame, result["resolvedSelector"]) + locator._locator_code = result.get("locatorCode") + return locator + + async def scroll_into_view_if_needed( + self, + timeout: float = None, + ) -> None: + return await self._with_element( + lambda h, timeout: h.scroll_into_view_if_needed(timeout=timeout), + timeout, + ) + + async def select_option( + self, + value: Union[str, Sequence[str]] = None, + index: Union[int, Sequence[int]] = None, + label: Union[str, Sequence[str]] = None, + element: Union["ElementHandle", Sequence["ElementHandle"]] = None, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + ) -> List[str]: + params = locals_to_params(locals()) + return await self._frame.select_option( + self._selector, + strict=True, + **params, + ) + + async def select_text(self, force: bool = None, timeout: float = None) -> None: + params = locals_to_params(locals()) + return await self._with_element( + lambda h, timeout: h.select_text(**{**params, "timeout": timeout}), + timeout, + ) + + async def set_input_files( + self, + files: Union[ + str, + pathlib.Path, + FilePayload, + Sequence[Union[str, pathlib.Path]], + Sequence[FilePayload], + ], + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.set_input_files( + self._selector, + strict=True, + **params, + ) + + async def tap( + self, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.tap( + self._selector, + strict=True, + **params, + ) + + async def text_content(self, timeout: float = None) -> Optional[str]: + params = locals_to_params(locals()) + return await self._frame.text_content( + self._selector, + strict=True, + **params, + ) + + async def type( + self, + text: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.type( + self._selector, + strict=True, + **params, + ) + + async def press_sequentially( + self, + text: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + await self.type(text, delay=delay, timeout=timeout) + + async def uncheck( + self, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.uncheck( + self._selector, + strict=True, + **params, + ) + + async def all_inner_texts( + self, + ) -> List[str]: + return await self._frame.eval_on_selector_all( + self._selector, "ee => ee.map(e => e.innerText)" + ) + + async def all_text_contents( + self, + ) -> List[str]: + return await self._frame.eval_on_selector_all( + self._selector, "ee => ee.map(e => e.textContent || '')" + ) + + async def wait_for( + self, + timeout: float = None, + state: Literal["attached", "detached", "hidden", "visible"] = None, + ) -> None: + await self._frame.wait_for_selector( + self._selector, strict=True, timeout=timeout, state=state + ) + + async def wait_for_function( + self, + expression: str, + arg: Serializable = None, + timeout: float = None, + ) -> None: + await self._frame._channel.send( + "waitForFunction", + self._frame._timeout, + { + "selector": self._selector, + "strict": True, + "expression": expression, + "arg": serialize_argument(arg), + "timeout": timeout, + }, + ) + + async def set_checked( + self, + checked: bool, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + if checked: + await self.check( + position=position, + timeout=timeout, + force=force, + trial=trial, + scroll=scroll, + ) + else: + await self.uncheck( + position=position, + timeout=timeout, + force=force, + trial=trial, + scroll=scroll, + ) + + async def _expect( + self, + expression: str, + options: FrameExpectOptions, + title: str = None, + ) -> FrameExpectResult: + return await self._frame._expect(self._selector, expression, options, title) + + async def highlight(self, style: str = None) -> None: + await self._frame._highlight(self._selector, style) + + async def hide_highlight(self) -> None: + await self._frame._hide_highlight(self._selector) + + +ANY_FRAME_SELECTOR = "internal:control=any-frame" + + +class FrameLocator: + def __init__(self, frame: "Frame", frame_selector: str) -> None: + self._frame = frame + self._loop = frame._loop + self._dispatcher_fiber = frame._connection._dispatcher_fiber + self._frame_selector = frame_selector + + def _child_selector(self, selector: str) -> str: + if self._frame_selector == ANY_FRAME_SELECTOR: + return f"{self._frame_selector} >> {selector}" + return f"{self._frame_selector} >> internal:control=enter-frame >> {selector}" + + def _nth_selector(self, nth: str) -> str: + if self._frame_selector == ANY_FRAME_SELECTOR: + raise Error("Selecting the nth frame is not allowed on frame_locator().") + return f"{self._frame_selector} >> nth={nth}" + + def locator( + self, + selectorOrLocator: Union["Locator", str], + hasText: Union[str, Pattern[str]] = None, + hasNotText: Union[str, Pattern[str]] = None, + has: Locator = None, + hasNot: Locator = None, + ) -> Locator: + if isinstance(selectorOrLocator, str): + return Locator( + self._frame, + self._child_selector(selectorOrLocator), + has_text=hasText, + has_not_text=hasNotText, + has=has, + has_not=hasNot, + ) + selectorOrLocator = to_impl(selectorOrLocator) + if selectorOrLocator._frame != self._frame: + raise ValueError("Locators must belong to the same frame.") + return Locator( + self._frame, + self._child_selector(selectorOrLocator._selector), + has_text=hasText, + has_not_text=hasNotText, + has=has, + has_not=hasNot, + ) + + def get_by_alt_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_alt_text_selector(text, exact=exact)) + + def get_by_label( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_label_selector(text, exact=exact)) + + def get_by_placeholder( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_placeholder_selector(text, exact=exact)) + + def get_by_role( + self, + role: AriaRole, + checked: bool = None, + disabled: bool = None, + expanded: bool = None, + includeHidden: bool = None, + level: int = None, + name: Union[str, Pattern[str]] = None, + pressed: bool = None, + selected: bool = None, + exact: bool = None, + description: Union[str, Pattern[str]] = None, + ) -> "Locator": + return self.locator( + get_by_role_selector( + role, + checked=checked, + disabled=disabled, + expanded=expanded, + includeHidden=includeHidden, + level=level, + name=name, + pressed=pressed, + selected=selected, + exact=exact, + description=description, + ) + ) + + def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": + return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId)) + + def get_by_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_text_selector(text, exact=exact)) + + def get_by_title( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self.locator(get_by_title_selector(text, exact=exact)) + + def frame_locator(self, selector: str) -> "FrameLocator": + return FrameLocator(self._frame, self._child_selector(selector)) + + @property + def first(self) -> "FrameLocator": + return FrameLocator(self._frame, self._nth_selector("0")) + + @property + def last(self) -> "FrameLocator": + return FrameLocator(self._frame, self._nth_selector("-1")) + + @property + def owner(self) -> "Locator": + return Locator(self._frame, self._frame_selector) + + def nth(self, index: int) -> "FrameLocator": + return FrameLocator(self._frame, self._nth_selector(str(index))) + + def __repr__(self) -> str: + return f"" + + +_test_id_attribute_name: str = "data-testid" + + +def test_id_attribute_name() -> str: + return _test_id_attribute_name + + +def set_test_id_attribute_name(attribute_name: str) -> None: + global _test_id_attribute_name + _test_id_attribute_name = attribute_name + + +def get_by_test_id_selector( + test_id_attribute_name: str, test_id: Union[str, Pattern[str]] +) -> str: + return f"internal:testid=[{test_id_attribute_name}={escape_for_attribute_selector(test_id, True)}]" + + +def get_by_attribute_text_selector( + attr_name: str, text: Union[str, Pattern[str]], exact: bool = None +) -> str: + return f"internal:attr=[{attr_name}={escape_for_attribute_selector(text, exact=exact)}]" + + +def get_by_label_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return "internal:label=" + escape_for_text_selector(text, exact=exact) + + +def get_by_alt_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return get_by_attribute_text_selector("alt", text, exact=exact) + + +def get_by_title_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return get_by_attribute_text_selector("title", text, exact=exact) + + +def get_by_placeholder_selector( + text: Union[str, Pattern[str]], exact: bool = None +) -> str: + return get_by_attribute_text_selector("placeholder", text, exact=exact) + + +def get_by_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str: + return "internal:text=" + escape_for_text_selector(text, exact=exact) + + +def bool_to_js_bool(value: bool) -> str: + return "true" if value else "false" + + +def get_by_role_selector( + role: AriaRole, + checked: bool = None, + disabled: bool = None, + expanded: bool = None, + includeHidden: bool = None, + level: int = None, + name: Union[str, Pattern[str]] = None, + pressed: bool = None, + selected: bool = None, + exact: bool = None, + description: Union[str, Pattern[str]] = None, +) -> str: + props: List[Tuple[str, str]] = [] + if checked is not None: + props.append(("checked", bool_to_js_bool(checked))) + if disabled is not None: + props.append(("disabled", bool_to_js_bool(disabled))) + if selected is not None: + props.append(("selected", bool_to_js_bool(selected))) + if expanded is not None: + props.append(("expanded", bool_to_js_bool(expanded))) + if includeHidden is not None: + props.append(("include-hidden", bool_to_js_bool(includeHidden))) + if level is not None: + props.append(("level", str(level))) + if name is not None: + props.append( + ( + "name", + escape_for_attribute_selector(name, exact=exact), + ) + ) + if description is not None: + props.append( + ( + "description", + escape_for_attribute_selector(description, exact=exact), + ) + ) + if pressed is not None: + props.append(("pressed", bool_to_js_bool(pressed))) + props_str = "".join([f"[{t[0]}={t[1]}]" for t in props]) + return f"internal:role={role}{props_str}" diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_map.py b/venv/lib/python3.11/site-packages/playwright/_impl/_map.py new file mode 100644 index 0000000..95c05f4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_map.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict, Generic, Tuple, TypeVar + +K = TypeVar("K") +V = TypeVar("V") + + +class Map(Generic[K, V]): + def __init__(self) -> None: + self._entries: Dict[int, Tuple[K, V]] = {} + + def __contains__(self, item: K) -> bool: + return id(item) in self._entries + + def __setitem__(self, idx: K, value: V) -> None: + self._entries[id(idx)] = (idx, value) + + def __getitem__(self, obj: K) -> V: + return self._entries[id(obj)][1] diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_network.py b/venv/lib/python3.11/site-packages/playwright/_impl/_network.py new file mode 100644 index 0000000..0180419 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_network.py @@ -0,0 +1,1058 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import base64 +import json +import json as json_utils +import mimetypes +import re +from pathlib import Path +from types import SimpleNamespace +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Coroutine, + Dict, + List, + Optional, + Pattern, + Sequence, + TypedDict, + Union, + cast, +) +from urllib import parse + +from playwright._impl._api_structures import ( + ClientCertificate, + HttpCredentials, + RemoteAddr, + RequestSizes, + ResourceTiming, + SecurityDetails, +) +from playwright._impl._connection import ( + ChannelOwner, + _capture_stack_trace, + from_channel, + from_nullable_channel, +) +from playwright._impl._errors import Error +from playwright._impl._event_context_manager import EventContextManagerImpl +from playwright._impl._helper import ( + URLMatch, + WebSocketRouteHandlerCallback, + async_readfile, + locals_to_params, + url_matches, +) +from playwright._impl._internal_structures import Headers, HeadersArray +from playwright._impl._str_utils import escape_regex_flags +from playwright._impl._waiter import Waiter + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser_context import BrowserContext + from playwright._impl._fetch import APIResponse + from playwright._impl._frame import Frame + from playwright._impl._page import Page, Worker + + +class FallbackOverrideParameters(TypedDict, total=False): + url: Optional[str] + method: Optional[str] + headers: Optional[Dict[str, str]] + postData: Optional[Union[str, bytes]] + + +class SerializedFallbackOverrides: + def __init__(self) -> None: + self.url: Optional[str] = None + self.method: Optional[str] = None + self.headers: Optional[Dict[str, str]] = None + self.post_data_buffer: Optional[bytes] = None + + +def serialize_headers(headers: Dict[str, str]) -> HeadersArray: + return [ + {"name": name, "value": value} + for name, value in headers.items() + if value is not None + ] + + +async def to_client_certificates_protocol( + clientCertificates: Optional[List[ClientCertificate]], +) -> Optional[List[Dict[str, str]]]: + if not clientCertificates: + return None + out = [] + for clientCertificate in clientCertificates: + out_record = { + "origin": clientCertificate["origin"], + } + if passphrase := clientCertificate.get("passphrase"): + out_record["passphrase"] = passphrase + if pfx := clientCertificate.get("pfx"): + out_record["pfx"] = base64.b64encode(pfx).decode() + if pfx_path := clientCertificate.get("pfxPath"): + out_record["pfx"] = base64.b64encode( + await async_readfile(pfx_path) + ).decode() + if cert := clientCertificate.get("cert"): + out_record["cert"] = base64.b64encode(cert).decode() + if cert_path := clientCertificate.get("certPath"): + out_record["cert"] = base64.b64encode( + await async_readfile(cert_path) + ).decode() + if key := clientCertificate.get("key"): + out_record["key"] = base64.b64encode(key).decode() + if key_path := clientCertificate.get("keyPath"): + out_record["key"] = base64.b64encode( + await async_readfile(key_path) + ).decode() + out.append(out_record) + return out + + +def to_http_credentials_protocol( + httpCredentials: Optional[Union[HttpCredentials, Sequence[HttpCredentials]]], +) -> Optional[List[HttpCredentials]]: + if not httpCredentials: + return None + credentials_list = ( + [httpCredentials] if isinstance(httpCredentials, dict) else httpCredentials + ) + return [ + cast(HttpCredentials, {k: v for k, v in c.items() if v is not None}) + for c in credentials_list + ] or None + + +def to_clear_cookies_params( + name: Optional[Union[str, Pattern[str]]], + domain: Optional[Union[str, Pattern[str]]], + path: Optional[Union[str, Pattern[str]]], +) -> Dict[str, Optional[str]]: + return { + "name": name if isinstance(name, str) else None, + "nameRegexSource": name.pattern if isinstance(name, Pattern) else None, + "nameRegexFlags": ( + escape_regex_flags(name) if isinstance(name, Pattern) else None + ), + "domain": domain if isinstance(domain, str) else None, + "domainRegexSource": (domain.pattern if isinstance(domain, Pattern) else None), + "domainRegexFlags": ( + escape_regex_flags(domain) if isinstance(domain, Pattern) else None + ), + "path": path if isinstance(path, str) else None, + "pathRegexSource": path.pattern if isinstance(path, Pattern) else None, + "pathRegexFlags": ( + escape_regex_flags(path) if isinstance(path, Pattern) else None + ), + } + + +class Request(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._redirected_from: Optional["Request"] = from_nullable_channel( + initializer.get("redirectedFrom") + ) + self._redirected_to: Optional["Request"] = None + if self._redirected_from: + self._redirected_from._redirected_to = self + self._failure_text: Optional[str] = None + self._timing: ResourceTiming = { + "startTime": 0, + "domainLookupStart": -1, + "domainLookupEnd": -1, + "connectStart": -1, + "secureConnectionStart": -1, + "connectEnd": -1, + "requestStart": -1, + "responseStart": -1, + "responseEnd": -1, + } + self._provisional_headers = RawHeaders(self._initializer["headers"]) + self._all_headers_future: Optional[asyncio.Future[RawHeaders]] = None + self._fallback_overrides: SerializedFallbackOverrides = ( + SerializedFallbackOverrides() + ) + self._response: Optional["Response"] = None + + def __repr__(self) -> str: + return f"" + + def _apply_fallback_overrides(self, overrides: FallbackOverrideParameters) -> None: + self._fallback_overrides.url = overrides.get( + "url", self._fallback_overrides.url + ) + self._fallback_overrides.method = overrides.get( + "method", self._fallback_overrides.method + ) + self._fallback_overrides.headers = overrides.get( + "headers", self._fallback_overrides.headers + ) + post_data = overrides.get("postData") + if isinstance(post_data, str): + self._fallback_overrides.post_data_buffer = post_data.encode() + elif isinstance(post_data, bytes): + self._fallback_overrides.post_data_buffer = post_data + elif post_data is not None: + self._fallback_overrides.post_data_buffer = json.dumps(post_data).encode() + + @property + def url(self) -> str: + return cast(str, self._fallback_overrides.url or self._initializer["url"]) + + @property + def resource_type(self) -> str: + return self._initializer["resourceType"] + + @property + def service_worker(self) -> Optional["Worker"]: + return cast( + Optional["Worker"], + from_nullable_channel(self._initializer.get("serviceWorker")), + ) + + @property + def method(self) -> str: + return cast(str, self._fallback_overrides.method or self._initializer["method"]) + + async def sizes(self) -> RequestSizes: + response = await self.response() + if not response: + raise Error("Unable to fetch sizes for failed request") + return await response._channel.send( + "sizes", + None, + ) + + @property + def post_data(self) -> Optional[str]: + data = self._fallback_overrides.post_data_buffer + if data is not None: + return data.decode() + base64_post_data = self._initializer.get("postData") + if base64_post_data is not None: + return base64.b64decode(base64_post_data).decode() + return None + + @property + def post_data_json(self) -> Optional[Any]: + post_data = self.post_data + if not post_data: + return None + content_type = self.headers.get("content-type") + if content_type and "application/x-www-form-urlencoded" in content_type: + return dict(parse.parse_qsl(post_data, keep_blank_values=True)) + try: + return json.loads(post_data) + except Exception: + raise Error(f"POST data is not a valid JSON object: {post_data}") + + @property + def post_data_buffer(self) -> Optional[bytes]: + if self._fallback_overrides.post_data_buffer: + return self._fallback_overrides.post_data_buffer + if self._initializer.get("postData"): + return base64.b64decode(self._initializer["postData"]) + return None + + async def response(self) -> Optional["Response"]: + return from_nullable_channel( + await self._channel.send( + "response", + None, + ) + ) + + @property + def existing_response(self) -> Optional["Response"]: + return self._response + + @property + def frame(self) -> "Frame": + if not self._initializer.get("frame"): + raise Error("Service Worker requests do not have an associated frame.") + frame = cast("Frame", from_channel(self._initializer["frame"])) + if not frame._page: + raise Error( + "\n".join( + [ + "Frame for this navigation request is not available, because the request", + "was issued before the frame is created. You can check whether the request", + "is a navigation request by calling isNavigationRequest() method.", + ] + ) + ) + return frame + + def is_navigation_request(self) -> bool: + return self._initializer["isNavigationRequest"] + + @property + def redirected_from(self) -> Optional["Request"]: + return self._redirected_from + + @property + def redirected_to(self) -> Optional["Request"]: + return self._redirected_to + + @property + def failure(self) -> Optional[str]: + return self._failure_text + + @property + def timing(self) -> ResourceTiming: + return self._timing + + def _set_response_end_timing(self, response_end_timing: float) -> None: + self._timing["responseEnd"] = response_end_timing + if self._timing["responseStart"] == -1: + self._timing["responseStart"] = response_end_timing + + @property + def headers(self) -> Headers: + override = self._fallback_overrides.headers + if override: + return RawHeaders._from_headers_dict_lossy(override).headers() + return self._provisional_headers.headers() + + async def all_headers(self) -> Headers: + return (await self._actual_headers()).headers() + + async def headers_array(self) -> HeadersArray: + return (await self._actual_headers()).headers_array() + + async def header_value(self, name: str) -> Optional[str]: + return (await self._actual_headers()).get(name) + + async def _actual_headers(self) -> "RawHeaders": + override = self._fallback_overrides.headers + if override: + return RawHeaders(serialize_headers(override)) + if not self._all_headers_future: + self._all_headers_future = asyncio.Future() + headers = await self._channel.send( + "rawRequestHeaders", None, is_internal=True + ) + self._all_headers_future.set_result(RawHeaders(headers)) + return await self._all_headers_future + + def _target_closed_future(self) -> asyncio.Future: + frame = cast( + Optional["Frame"], from_nullable_channel(self._initializer.get("frame")) + ) + if not frame: + return asyncio.Future() + page = frame._page + if not page: + return asyncio.Future() + return page._closed_or_crashed_future + + def _safe_page(self) -> "Optional[Page]": + frame = from_nullable_channel(self._initializer.get("frame")) + if not frame: + return None + return cast("Frame", frame)._page + + +class Route(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._handling_future: Optional[asyncio.Future["bool"]] = None + self._context: "BrowserContext" = cast("BrowserContext", None) + self._did_throw = False + + def _start_handling(self) -> "asyncio.Future[bool]": + self._handling_future = asyncio.Future() + return self._handling_future + + def _report_handled(self, done: bool) -> None: + chain = self._handling_future + assert chain + self._handling_future = None + chain.set_result(done) + + def _check_not_handled(self) -> None: + if not self._handling_future: + raise Error("Route is already handled!") + + def __repr__(self) -> str: + return f"" + + @property + def request(self) -> Request: + return from_channel(self._initializer["request"]) + + async def abort(self, errorCode: str = None) -> None: + await self._handle_route( + lambda: self._race_with_page_close( + self._channel.send( + "abort", + None, + { + "errorCode": errorCode, + }, + ) + ) + ) + + async def fulfill( + self, + status: int = None, + headers: Dict[str, str] = None, + body: Union[str, bytes] = None, + json: Any = None, + path: Union[str, Path] = None, + contentType: str = None, + response: "APIResponse" = None, + ) -> None: + await self._handle_route( + lambda: self._inner_fulfill( + status, headers, body, json, path, contentType, response + ) + ) + + async def _inner_fulfill( + self, + status: int = None, + headers: Dict[str, str] = None, + body: Union[str, bytes] = None, + json: Any = None, + path: Union[str, Path] = None, + contentType: str = None, + response: "APIResponse" = None, + ) -> None: + params = locals_to_params(locals()) + + if json is not None: + if body is not None: + raise Error("Can specify either body or json parameters") + body = json_utils.dumps(json) + + if response: + del params["response"] + params["status"] = ( + params["status"] if params.get("status") else response.status + ) + params["headers"] = ( + params["headers"] if params.get("headers") else response.headers + ) + from playwright._impl._fetch import APIResponse + + if body is None and path is None and isinstance(response, APIResponse): + if response._request._connection is self._connection: + params["fetchResponseUid"] = response._fetch_uid + else: + body = await response.body() + + length = 0 + if isinstance(body, str): + params["body"] = body + params["isBase64"] = False + length = len(body.encode()) + elif isinstance(body, bytes): + params["body"] = base64.b64encode(body).decode() + params["isBase64"] = True + length = len(body) + elif path: + del params["path"] + file_content = Path(path).read_bytes() + params["body"] = base64.b64encode(file_content).decode() + params["isBase64"] = True + length = len(file_content) + + headers = {k.lower(): str(v) for k, v in params.get("headers", {}).items()} + if params.get("contentType"): + headers["content-type"] = params["contentType"] + elif json or isinstance(json, (list, tuple, dict)): + # Like the JS client, which checks truthiness: [] and {} are truthy in JS. + headers["content-type"] = "application/json" + elif path: + headers["content-type"] = ( + mimetypes.guess_type(str(Path(path)))[0] or "application/octet-stream" + ) + if length and "content-length" not in headers: + headers["content-length"] = str(length) + params["headers"] = serialize_headers(headers) + + await self._race_with_page_close(self._channel.send("fulfill", None, params)) + + async def _handle_route(self, callback: Callable) -> None: + self._check_not_handled() + try: + await callback() + self._report_handled(True) + except Exception as e: + self._did_throw = True + raise e + + async def fetch( + self, + url: str = None, + method: str = None, + headers: Dict[str, str] = None, + postData: Union[Any, str, bytes] = None, + maxRedirects: int = None, + maxRetries: int = None, + timeout: float = None, + ) -> "APIResponse": + return await self._connection.wrap_api_call( + lambda: self._context.request._inner_fetch( + self.request, + url, + method, + headers, + postData, + maxRedirects=maxRedirects, + maxRetries=maxRetries, + timeout=timeout, + ) + ) + + async def fallback( + self, + url: str = None, + method: str = None, + headers: Dict[str, str] = None, + postData: Union[Any, str, bytes] = None, + ) -> None: + overrides = cast(FallbackOverrideParameters, locals_to_params(locals())) + self._check_not_handled() + self.request._apply_fallback_overrides(overrides) + self._report_handled(False) + + async def continue_( + self, + url: str = None, + method: str = None, + headers: Dict[str, str] = None, + postData: Union[Any, str, bytes] = None, + ) -> None: + overrides = cast(FallbackOverrideParameters, locals_to_params(locals())) + + async def _inner() -> None: + self.request._apply_fallback_overrides(overrides) + await self._inner_continue(False) + + return await self._handle_route(_inner) + + async def _inner_continue(self, is_fallback: bool = False) -> None: + options = self.request._fallback_overrides + await self._race_with_page_close( + self._channel.send( + "continue", + None, + { + "url": options.url, + "method": options.method, + "headers": ( + serialize_headers(options.headers) if options.headers else None + ), + "postData": ( + base64.b64encode(options.post_data_buffer).decode() + if options.post_data_buffer is not None + else None + ), + "isFallback": is_fallback, + }, + ) + ) + + async def _redirected_navigation_request(self, url: str) -> None: + await self._handle_route( + lambda: self._race_with_page_close( + self._channel.send("redirectNavigationRequest", None, {"url": url}) + ) + ) + + async def _race_with_page_close(self, future: Coroutine) -> None: + fut = asyncio.create_task(future) + # Rewrite the user's stack to the new task which runs in the background. + setattr( + fut, + "__pw_stack__", + getattr(asyncio.current_task(self._loop), "__pw_stack__", None) + or _capture_stack_trace(), + ) + target_closed_future = self.request._target_closed_future() + await asyncio.wait( + [fut, target_closed_future], + return_when=asyncio.FIRST_COMPLETED, + ) + if fut.done() and fut.exception(): + raise cast(BaseException, fut.exception()) + if target_closed_future.done(): + await asyncio.gather(fut, return_exceptions=True) + + +class ServerWebSocketRoute: + def __init__(self, ws: "WebSocketRoute"): + self._ws = ws + + def on_message(self, handler: Callable[[Union[str, bytes]], Any]) -> None: + self._ws._on_server_message = handler + + def on_close(self, handler: Callable[[Optional[int], Optional[str]], Any]) -> None: + self._ws._on_server_close = handler + + def connect_to_server(self) -> None: + raise NotImplementedError( + "connectToServer must be called on the page-side WebSocketRoute" + ) + + @property + def url(self) -> str: + return self._ws._initializer["url"] + + @property + def protocols(self) -> List[str]: + return list(self._ws._initializer.get("protocols", [])) + + def close(self, code: int = None, reason: str = None) -> None: + self._ws._channel.send_may_fail( + "closeServer", + None, + { + "code": code, + "reason": reason, + "wasClean": True, + }, + ) + + def send(self, message: Union[str, bytes]) -> None: + if isinstance(message, str): + self._ws._channel.send_may_fail( + "sendToServer", None, {"message": message, "isBase64": False} + ) + else: + self._ws._channel.send_may_fail( + "sendToServer", + None, + {"message": base64.b64encode(message).decode(), "isBase64": True}, + ) + + +class WebSocketRoute(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._on_page_message: Optional[Callable[[Union[str, bytes]], Any]] = None + self._on_page_close: Optional[Callable[[Optional[int], Optional[str]], Any]] = ( + None + ) + self._on_server_message: Optional[Callable[[Union[str, bytes]], Any]] = None + self._on_server_close: Optional[ + Callable[[Optional[int], Optional[str]], Any] + ] = None + self._server = ServerWebSocketRoute(self) + self._connected = False + + self._channel.on("messageFromPage", self._channel_message_from_page) + self._channel.on("messageFromServer", self._channel_message_from_server) + self._channel.on("closePage", self._channel_close_page) + self._channel.on("closeServer", self._channel_close_server) + + def _channel_message_from_page(self, event: Dict) -> None: + if self._on_page_message: + self._on_page_message( + base64.b64decode(event["message"]) + if event["isBase64"] + else event["message"] + ) + elif self._connected: + self._channel.send_may_fail("sendToServer", None, event) + + def _channel_message_from_server(self, event: Dict) -> None: + if self._on_server_message: + self._on_server_message( + base64.b64decode(event["message"]) + if event["isBase64"] + else event["message"] + ) + else: + self._channel.send_may_fail("sendToPage", None, event) + + def _channel_close_page(self, event: Dict) -> None: + if self._on_page_close: + self._on_page_close(event["code"], event["reason"]) + else: + self._channel.send_may_fail("closeServer", None, event) + + def _channel_close_server(self, event: Dict) -> None: + if self._on_server_close: + self._on_server_close(event["code"], event["reason"]) + else: + self._channel.send_may_fail("closePage", None, event) + + @property + def url(self) -> str: + return self._initializer["url"] + + @property + def protocols(self) -> List[str]: + return list(self._initializer.get("protocols", [])) + + async def close(self, code: int = None, reason: str = None) -> None: + try: + await self._channel.send( + "closePage", None, {"code": code, "reason": reason, "wasClean": True} + ) + except Exception: + pass + + def connect_to_server(self) -> "WebSocketRoute": + if self._connected: + raise Error("Already connected to the server") + self._connected = True + self._channel.send_may_fail("connect", None) + return cast("WebSocketRoute", self._server) + + def send(self, message: Union[str, bytes]) -> None: + if isinstance(message, str): + self._channel.send_may_fail( + "sendToPage", None, {"message": message, "isBase64": False} + ) + else: + self._channel.send_may_fail( + "sendToPage", + None, + { + "message": base64.b64encode(message).decode(), + "isBase64": True, + }, + ) + + def on_message(self, handler: Callable[[Union[str, bytes]], Any]) -> None: + self._on_page_message = handler + + def on_close(self, handler: Callable[[Optional[int], Optional[str]], Any]) -> None: + self._on_page_close = handler + + async def _after_handle(self) -> None: + if self._connected: + return + # Ensure that websocket is "open" and can send messages without an actual server connection. + try: + await self._channel.send( + "ensureOpened", + None, + ) + except Exception: + pass + + +class WebSocketRouteHandler: + def __init__( + self, + base_url: Optional[str], + url: URLMatch, + handler: WebSocketRouteHandlerCallback, + ): + self._base_url = base_url + self.url = url + self.handler = handler + + @staticmethod + def prepare_interception_patterns( + handlers: List["WebSocketRouteHandler"], + ) -> List[dict]: + patterns = [] + all_urls = False + for handler in handlers: + if isinstance(handler.url, str): + patterns.append({"glob": handler.url}) + elif isinstance(handler.url, re.Pattern): + patterns.append( + { + "regexSource": handler.url.pattern, + "regexFlags": escape_regex_flags(handler.url), + } + ) + else: + all_urls = True + + if all_urls: + return [{"glob": "**/*"}] + return patterns + + def matches(self, ws_url: str) -> bool: + return url_matches(self._base_url, ws_url, self.url, True) + + async def handle(self, websocket_route: "WebSocketRoute") -> None: + coro_or_future = self.handler(websocket_route) + if asyncio.iscoroutine(coro_or_future): + await coro_or_future + await websocket_route._after_handle() + + +class Response(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._request: Request = from_channel(self._initializer["request"]) + self._request._response = self + timing = self._initializer["timing"] + self._request._timing["startTime"] = timing["startTime"] + self._request._timing["domainLookupStart"] = timing["domainLookupStart"] + self._request._timing["domainLookupEnd"] = timing["domainLookupEnd"] + self._request._timing["connectStart"] = timing["connectStart"] + self._request._timing["secureConnectionStart"] = timing["secureConnectionStart"] + self._request._timing["connectEnd"] = timing["connectEnd"] + self._request._timing["requestStart"] = timing["requestStart"] + self._request._timing["responseStart"] = timing["responseStart"] + self._provisional_headers = RawHeaders( + cast(HeadersArray, self._initializer["headers"]) + ) + self._raw_headers_future: Optional[asyncio.Future[RawHeaders]] = None + self._finished_future: asyncio.Future[bool] = asyncio.Future() + + def __repr__(self) -> str: + return f"" + + @property + def url(self) -> str: + return self._initializer["url"] + + @property + def ok(self) -> bool: + # Status 0 is for file:// URLs + return self._initializer["status"] == 0 or ( + self._initializer["status"] >= 200 and self._initializer["status"] <= 299 + ) + + @property + def status(self) -> int: + return self._initializer["status"] + + @property + def status_text(self) -> str: + return self._initializer["statusText"] + + @property + def headers(self) -> Headers: + return self._provisional_headers.headers() + + @property + def from_service_worker(self) -> bool: + return self._initializer["fromServiceWorker"] + + async def all_headers(self) -> Headers: + return (await self._actual_headers()).headers() + + async def headers_array(self) -> HeadersArray: + return (await self._actual_headers()).headers_array() + + async def header_value(self, name: str) -> Optional[str]: + return (await self._actual_headers()).get(name) + + async def header_values(self, name: str) -> List[str]: + return (await self._actual_headers()).get_all(name) + + async def _actual_headers(self) -> "RawHeaders": + if not self._raw_headers_future: + self._raw_headers_future = asyncio.Future() + headers = cast( + HeadersArray, + await self._channel.send( + "rawResponseHeaders", + None, + ), + ) + self._raw_headers_future.set_result(RawHeaders(headers)) + return await self._raw_headers_future + + async def server_addr(self) -> Optional[RemoteAddr]: + return await self._channel.send( + "serverAddr", + None, + ) + + async def security_details(self) -> Optional[SecurityDetails]: + return await self._channel.send( + "securityDetails", + None, + ) + + async def http_version(self) -> str: + return await self._channel.send( + "httpVersion", + None, + ) + + async def finished(self) -> None: + async def on_finished() -> None: + await self._request._target_closed_future() + raise Error("Target closed") + + on_finished_task = asyncio.create_task(on_finished()) + await asyncio.wait( + cast( + List[Union[asyncio.Task, asyncio.Future]], + [self._finished_future, on_finished_task], + ), + return_when=asyncio.FIRST_COMPLETED, + ) + if on_finished_task.done(): + await on_finished_task + + async def body(self) -> bytes: + binary = await self._channel.send( + "body", + None, + ) + return base64.b64decode(binary) + + async def text(self) -> str: + content = await self.body() + return content.decode() + + async def json(self) -> Any: + return json.loads(await self.text()) + + @property + def request(self) -> Request: + return self._request + + @property + def frame(self) -> "Frame": + return self._request.frame + + +class WebSocket(ChannelOwner): + Events = SimpleNamespace( + Close="close", + FrameReceived="framereceived", + FrameSent="framesent", + Error="socketerror", + ) + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._is_closed = False + self._page = cast("Page", parent) + self._channel.on( + "frameSent", + lambda params: self._on_frame_sent(params["opcode"], params["data"]), + ) + self._channel.on( + "frameReceived", + lambda params: self._on_frame_received(params["opcode"], params["data"]), + ) + self._channel.on( + "socketError", + lambda params: self.emit(WebSocket.Events.Error, params["error"]), + ) + self._channel.on("close", lambda params: self._on_close()) + + def __repr__(self) -> str: + return f"" + + @property + def url(self) -> str: + return self._initializer["url"] + + def expect_event( + self, + event: str, + predicate: Callable = None, + timeout: float = None, + ) -> EventContextManagerImpl: + if timeout is None: + timeout = cast(Any, self._parent)._timeout_settings.timeout() + waiter = Waiter(self, f"web_socket.expect_event({event})") + waiter.reject_on_timeout( + cast(float, timeout), + f'Timeout {timeout}ms exceeded while waiting for event "{event}"', + ) + if event != WebSocket.Events.Close: + waiter.reject_on_event(self, WebSocket.Events.Close, Error("Socket closed")) + if event != WebSocket.Events.Error: + waiter.reject_on_event(self, WebSocket.Events.Error, Error("Socket error")) + waiter.reject_on_event( + self._page, "close", lambda: self._page._close_error_with_reason() + ) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) + + async def wait_for_event( + self, event: str, predicate: Callable = None, timeout: float = None + ) -> Any: + async with self.expect_event(event, predicate, timeout) as event_info: + pass + return await event_info + + def _on_frame_sent(self, opcode: int, data: str) -> None: + if opcode == 2: + self.emit(WebSocket.Events.FrameSent, base64.b64decode(data)) + elif opcode == 1: + self.emit(WebSocket.Events.FrameSent, data) + + def _on_frame_received(self, opcode: int, data: str) -> None: + if opcode == 2: + self.emit(WebSocket.Events.FrameReceived, base64.b64decode(data)) + elif opcode == 1: + self.emit(WebSocket.Events.FrameReceived, data) + + def is_closed(self) -> bool: + return self._is_closed + + def _on_close(self) -> None: + self._is_closed = True + self.emit(WebSocket.Events.Close, self) + + +class RawHeaders: + def __init__(self, headers: HeadersArray) -> None: + self._headers_array = headers + self._headers_map: Dict[str, Dict[str, bool]] = {} + for header in headers: + values = self._headers_map.setdefault(header["name"].lower(), {}) + values[header["value"]] = True + + @staticmethod + def _from_headers_dict_lossy(headers: Dict[str, str]) -> "RawHeaders": + return RawHeaders(serialize_headers(headers)) + + def get(self, name: str) -> Optional[str]: + values = self.get_all(name) + if not values: + return None + separator = "\n" if name.lower() == "set-cookie" else ", " + return separator.join(values) + + def get_all(self, name: str) -> List[str]: + return list(self._headers_map.get(name.lower(), {}).keys()) + + def headers(self) -> Dict[str, str]: + result = {} + for name in self._headers_map.keys(): + result[name] = cast(str, self.get(name)) + return result + + def headers_array(self) -> HeadersArray: + return self._headers_array diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_object_factory.py b/venv/lib/python3.11/site-packages/playwright/_impl/_object_factory.py new file mode 100644 index 0000000..7911ddb --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_object_factory.py @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, cast + +from playwright._impl._artifact import Artifact +from playwright._impl._browser import Browser +from playwright._impl._browser_context import BrowserContext +from playwright._impl._browser_type import BrowserType +from playwright._impl._cdp_session import CDPSession +from playwright._impl._connection import ChannelOwner +from playwright._impl._debugger import Debugger +from playwright._impl._dialog import Dialog +from playwright._impl._disposable import Disposable +from playwright._impl._element_handle import ElementHandle +from playwright._impl._fetch import APIRequestContext +from playwright._impl._frame import Frame +from playwright._impl._js_handle import JSHandle +from playwright._impl._local_utils import LocalUtils +from playwright._impl._network import ( + Request, + Response, + Route, + WebSocket, + WebSocketRoute, +) +from playwright._impl._page import BindingCall, Page, Worker +from playwright._impl._playwright import Playwright +from playwright._impl._stream import Stream +from playwright._impl._tracing import Tracing +from playwright._impl._writable_stream import WritableStream + + +class DummyObject(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + + +def create_remote_object( + parent: ChannelOwner, type: str, guid: str, initializer: Dict +) -> ChannelOwner: + if type == "Artifact": + return Artifact(parent, type, guid, initializer) + if type == "APIRequestContext": + return APIRequestContext(parent, type, guid, initializer) + if type == "BindingCall": + return BindingCall(parent, type, guid, initializer) + if type == "Browser": + return Browser(cast(BrowserType, parent), type, guid, initializer) + if type == "BrowserType": + return BrowserType(parent, type, guid, initializer) + if type == "BrowserContext": + return BrowserContext(parent, type, guid, initializer) + if type == "CDPSession": + return CDPSession(parent, type, guid, initializer) + if type == "Debugger": + return Debugger(parent, type, guid, initializer) + if type == "Dialog": + return Dialog(parent, type, guid, initializer) + if type == "Disposable": + return Disposable(parent, type, guid, initializer) + if type == "ElementHandle": + return ElementHandle(parent, type, guid, initializer) + if type == "Frame": + return Frame(parent, type, guid, initializer) + if type == "JSHandle": + return JSHandle(parent, type, guid, initializer) + if type == "LocalUtils": + local_utils = LocalUtils(parent, type, guid, initializer) + if not local_utils._connection._local_utils: + local_utils._connection._local_utils = local_utils + return local_utils + if type == "Page": + return Page(parent, type, guid, initializer) + if type == "Playwright": + return Playwright(parent, type, guid, initializer) + if type == "Request": + return Request(parent, type, guid, initializer) + if type == "Response": + return Response(parent, type, guid, initializer) + if type == "Route": + return Route(parent, type, guid, initializer) + if type == "Stream": + return Stream(parent, type, guid, initializer) + if type == "Tracing": + return Tracing(parent, type, guid, initializer) + if type == "WebSocket": + return WebSocket(parent, type, guid, initializer) + if type == "WebSocketRoute": + return WebSocketRoute(parent, type, guid, initializer) + if type == "Worker": + return Worker(parent, type, guid, initializer) + if type == "WritableStream": + return WritableStream(parent, type, guid, initializer) + return DummyObject(parent, type, guid, initializer) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_page.py b/venv/lib/python3.11/site-packages/playwright/_impl/_page.py new file mode 100644 index 0000000..589bc35 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_page.py @@ -0,0 +1,1661 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import base64 +import inspect +import re +import sys +from pathlib import Path +from types import SimpleNamespace +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Dict, + List, + Literal, + Optional, + Pattern, + Sequence, + Union, + cast, +) + +from playwright._impl._api_structures import ( + FilePayload, + FloatRect, + PdfMargins, + Position, + ViewportSize, +) +from playwright._impl._artifact import Artifact +from playwright._impl._clock import Clock +from playwright._impl._connection import ( + ChannelOwner, + from_channel, + from_nullable_channel, +) +from playwright._impl._console_message import ConsoleMessage +from playwright._impl._disposable import Disposable, DisposableStub +from playwright._impl._download import Download +from playwright._impl._element_handle import ElementHandle, determine_screenshot_type +from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error +from playwright._impl._event_context_manager import EventContextManagerImpl +from playwright._impl._file_chooser import FileChooser +from playwright._impl._frame import Frame +from playwright._impl._greenlets import LocatorHandlerGreenlet +from playwright._impl._har_router import HarRouter +from playwright._impl._helper import ( + ColorScheme, + Contrast, + DocumentLoadState, + ForcedColors, + HarMode, + KeyboardModifier, + MouseButton, + ReducedMotion, + RouteFromHarNotFoundPolicy, + RouteHandler, + RouteHandlerCallback, + TimeoutSettings, + URLMatch, + URLMatchRequest, + URLMatchResponse, + WebSocketRouteHandlerCallback, + async_readfile, + async_writefile, + locals_to_params, + make_dirs_for_file, + parse_error, + serialize_error, + url_matches, +) +from playwright._impl._input import Keyboard, Mouse, Touchscreen +from playwright._impl._internal_structures import AriaRole +from playwright._impl._js_handle import ( + JSHandle, + Serializable, + add_source_url_to_script, + parse_result, + serialize_argument, +) +from playwright._impl._network import ( + Request, + Response, + Route, + WebSocketRoute, + WebSocketRouteHandler, + serialize_headers, +) +from playwright._impl._screencast import Screencast +from playwright._impl._signature import signature +from playwright._impl._video import Video +from playwright._impl._waiter import Waiter +from playwright._impl._web_storage import WebStorage +from playwright._impl._webmcp import WebMCP + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser_context import BrowserContext + from playwright._impl._fetch import APIRequestContext + from playwright._impl._locator import FrameLocator, Locator + from playwright._impl._network import WebSocket + + +class LocatorHandler: + locator: "Locator" + handler: Union[Callable[["Locator"], Any], Callable[..., Any]] + times: Union[int, None] + + def __init__( + self, locator: "Locator", handler: Callable[..., Any], times: Union[int, None] + ) -> None: + self.locator = locator + self._handler = handler + self.times = times + + def __call__(self) -> Any: + arg_count = len(signature(self._handler).parameters) + if arg_count == 0: + return self._handler() + return self._handler(self.locator) + + +class Page(ChannelOwner): + Events = SimpleNamespace( + Close="close", + Crash="crash", + Console="console", + Dialog="dialog", + DialogClosed="dialogclosed", + Download="download", + FileChooser="filechooser", + DOMContentLoaded="domcontentloaded", + PageError="pageerror", + Request="request", + Response="response", + RequestFailed="requestfailed", + RequestFinished="requestfinished", + FrameAttached="frameattached", + FrameDetached="framedetached", + FrameNavigated="framenavigated", + Load="load", + Popup="popup", + WebSocket="websocket", + Worker="worker", + ) + keyboard: Keyboard + mouse: Mouse + touchscreen: Touchscreen + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._browser_context = cast("BrowserContext", parent) + self.keyboard = Keyboard(self._channel) + self.mouse = Mouse(self._channel) + self.touchscreen = Touchscreen(self._channel) + + self._main_frame: Frame = from_channel(initializer["mainFrame"]) + self._main_frame._page = self + self._frames = [self._main_frame] + self._viewport_size: Optional[ViewportSize] = initializer.get("viewportSize") + self._is_closed = False + self._workers: List["Worker"] = [] + self._bindings: Dict[str, Any] = {} + self._routes: List[RouteHandler] = [] + self._web_socket_routes: List[WebSocketRouteHandler] = [] + self._owned_context: Optional["BrowserContext"] = None + self._timeout_settings: TimeoutSettings = TimeoutSettings( + self._browser_context._timeout_settings + ) + self._video: Video = Video( + self, + cast(Optional[Artifact], from_nullable_channel(initializer.get("video"))), + ) + self._screencast: Screencast = Screencast(self) + self._local_storage = WebStorage(self, "local") + self._session_storage = WebStorage(self, "session") + self._opener = cast("Page", from_nullable_channel(initializer.get("opener"))) + self._close_reason: Optional[str] = None + self._close_was_called = False + self._har_routers: List[HarRouter] = [] + self._locator_handlers: Dict[str, LocatorHandler] = {} + + self._channel.on( + "bindingCall", + lambda params: self._on_binding(from_channel(params["binding"])), + ) + self._channel.on("close", lambda _: self._on_close()) + self._channel.on("crash", lambda _: self._on_crash()) + self._channel.on("download", lambda params: self._on_download(params)) + self._channel.on( + "fileChooser", + lambda params: self.emit( + Page.Events.FileChooser, + FileChooser( + self, from_channel(params["element"]), params["isMultiple"] + ), + ), + ) + self._channel.on( + "frameAttached", + lambda params: self._on_frame_attached(from_channel(params["frame"])), + ) + self._channel.on( + "frameDetached", + lambda params: self._on_frame_detached(from_channel(params["frame"])), + ) + self._channel.on( + "locatorHandlerTriggered", + lambda params: self._loop.create_task( + self._on_locator_handler_triggered(params["uid"]) + ), + ) + self._channel.on( + "route", + lambda params: self._loop.create_task( + self._on_route(from_channel(params["route"])) + ), + ) + self._channel.on( + "webSocketRoute", + lambda params: self._loop.create_task( + self._on_web_socket_route(from_channel(params["webSocketRoute"])) + ), + ) + self._channel.on("viewportSizeChanged", self._on_viewport_size_changed) + self._channel.on( + "webSocket", + lambda params: self.emit( + Page.Events.WebSocket, from_channel(params["webSocket"]) + ), + ) + self._channel.on( + "worker", lambda params: self._on_worker(from_channel(params["worker"])) + ) + self._closed_or_crashed_future: asyncio.Future = asyncio.Future() + self.on( + Page.Events.Close, + lambda _: ( + self._closed_or_crashed_future.set_result( + self._close_error_with_reason() + ) + if not self._closed_or_crashed_future.done() + else None + ), + ) + self.on( + Page.Events.Crash, + lambda _: ( + self._closed_or_crashed_future.set_result(TargetClosedError()) + if not self._closed_or_crashed_future.done() + else None + ), + ) + + self._set_event_to_subscription_mapping( + { + Page.Events.Console: "console", + Page.Events.Dialog: "dialog", + Page.Events.DialogClosed: "dialogClosed", + Page.Events.Request: "request", + Page.Events.Response: "response", + Page.Events.RequestFinished: "requestFinished", + Page.Events.RequestFailed: "requestFailed", + Page.Events.FileChooser: "fileChooser", + } + ) + + def __repr__(self) -> str: + return f"" + + def _on_frame_attached(self, frame: Frame) -> None: + frame._page = self + self._frames.append(frame) + self.emit(Page.Events.FrameAttached, frame) + self._browser_context.emit("frameattached", frame) + + def _on_frame_detached(self, frame: Frame) -> None: + self._frames.remove(frame) + frame._detached = True + self.emit(Page.Events.FrameDetached, frame) + self._browser_context.emit("framedetached", frame) + + async def _on_route(self, route: Route) -> None: + route._context = self.context + route_handlers = self._routes.copy() + for route_handler in route_handlers: + # If the page was closed we stall all requests right away. + if self._close_was_called or self.context._closing_or_closed: + return + if not route_handler.matches(route.request.url): + continue + if route_handler not in self._routes: + continue + if route_handler.will_expire: + self._routes.remove(route_handler) + try: + handled = await route_handler.handle(route) + finally: + if len(self._routes) == 0: + + async def _update_interceptor_patterns_ignore_exceptions() -> None: + try: + await self._update_interception_patterns() + except Error: + pass + + asyncio.create_task( + self._connection.wrap_api_call( + _update_interceptor_patterns_ignore_exceptions, True + ) + ) + if handled: + return + await self._browser_context._on_route(route) + + async def _on_web_socket_route(self, web_socket_route: WebSocketRoute) -> None: + route_handler = next( + ( + route_handler + for route_handler in self._web_socket_routes + if route_handler.matches(web_socket_route.url) + ), + None, + ) + if route_handler: + await route_handler.handle(web_socket_route) + else: + await self._browser_context._on_web_socket_route(web_socket_route) + + def _on_binding(self, binding_call: "BindingCall") -> None: + func = self._bindings.get(binding_call._initializer["name"]) + if func: + asyncio.create_task(binding_call.call(func)) + self._browser_context._on_binding(binding_call) + + def _on_worker(self, worker: "Worker") -> None: + self._workers.append(worker) + worker._page = self + self.emit(Page.Events.Worker, worker) + + def _on_close(self) -> None: + self._is_closed = True + if self in self._browser_context._pages: + self._browser_context._pages.remove(self) + self._dispose_har_routers() + self.emit(Page.Events.Close, self) + self._browser_context.emit("pageclose", self) + + def _on_crash(self) -> None: + self.emit(Page.Events.Crash, self) + + def _on_download(self, params: Any) -> None: + url = params["url"] + suggested_filename = params["suggestedFilename"] + artifact = cast(Artifact, from_channel(params["artifact"])) + download = Download(self, url, suggested_filename, artifact) + self.emit(Page.Events.Download, download) + self._browser_context.emit("download", download) + + def _on_viewport_size_changed(self, params: Any) -> None: + self._viewport_size = params["viewportSize"] + + @property + def context(self) -> "BrowserContext": + return self._browser_context + + @property + def clock(self) -> Clock: + return self._browser_context.clock + + async def opener(self) -> Optional["Page"]: + if self._opener and self._opener.is_closed(): + return None + return self._opener + + @property + def main_frame(self) -> Frame: + return self._main_frame + + def frame(self, name: str = None, url: URLMatch = None) -> Optional[Frame]: + for frame in self._frames: + if name and frame.name == name: + return frame + if url and url_matches(self._browser_context._base_url, frame.url, url): + return frame + + return None + + @property + def frames(self) -> List[Frame]: + return self._frames.copy() + + def set_default_navigation_timeout(self, timeout: float) -> None: + self._timeout_settings.set_default_navigation_timeout(timeout) + + def set_default_timeout(self, timeout: float) -> None: + self._timeout_settings.set_default_timeout(timeout) + + async def query_selector( + self, + selector: str, + strict: bool = None, + ) -> Optional[ElementHandle]: + return await self._main_frame.query_selector(selector, strict) + + async def query_selector_all(self, selector: str) -> List[ElementHandle]: + return await self._main_frame.query_selector_all(selector) + + async def wait_for_selector( + self, + selector: str, + timeout: float = None, + state: Literal["attached", "detached", "hidden", "visible"] = None, + strict: bool = None, + ) -> Optional[ElementHandle]: + return await self._main_frame.wait_for_selector(**locals_to_params(locals())) + + async def is_checked( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._main_frame.is_checked(**locals_to_params(locals())) + + async def is_disabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._main_frame.is_disabled(**locals_to_params(locals())) + + async def is_editable( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._main_frame.is_editable(**locals_to_params(locals())) + + async def is_enabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + return await self._main_frame.is_enabled(**locals_to_params(locals())) + + async def is_hidden( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + # timeout is deprecated and does nothing + return await self._main_frame.is_hidden(selector=selector, strict=strict) + + async def is_visible( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: + # timeout is deprecated and does nothing + return await self._main_frame.is_visible(selector=selector, strict=strict) + + async def dispatch_event( + self, + selector: str, + type: str, + eventInit: Dict = None, + timeout: float = None, + strict: bool = None, + ) -> None: + return await self._main_frame.dispatch_event(**locals_to_params(locals())) + + async def evaluate(self, expression: str, arg: Serializable = None) -> Any: + return await self._main_frame.evaluate(expression, arg) + + async def evaluate_handle( + self, expression: str, arg: Serializable = None + ) -> JSHandle: + return await self._main_frame.evaluate_handle(expression, arg) + + async def eval_on_selector( + self, + selector: str, + expression: str, + arg: Serializable = None, + strict: bool = None, + ) -> Any: + return await self._main_frame.eval_on_selector( + selector, expression, arg, strict + ) + + async def eval_on_selector_all( + self, + selector: str, + expression: str, + arg: Serializable = None, + ) -> Any: + return await self._main_frame.eval_on_selector_all(selector, expression, arg) + + async def add_script_tag( + self, + url: str = None, + path: Union[str, Path] = None, + content: str = None, + type: str = None, + ) -> ElementHandle: + return await self._main_frame.add_script_tag(**locals_to_params(locals())) + + async def add_style_tag( + self, url: str = None, path: Union[str, Path] = None, content: str = None + ) -> ElementHandle: + return await self._main_frame.add_style_tag(**locals_to_params(locals())) + + async def expose_function(self, name: str, callback: Callable) -> Disposable: + return await self.expose_binding(name, lambda source, *args: callback(*args)) + + async def expose_binding(self, name: str, callback: Callable) -> Disposable: + if name in self._bindings: + raise Error(f'Function "{name}" has been already registered') + if name in self._browser_context._bindings: + raise Error( + f'Function "{name}" has been already registered in the browser context' + ) + self._bindings[name] = callback + return from_channel( + await self._channel.send( + "exposeBinding", + None, + dict(name=name), + ) + ) + + async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: + await self._channel.send( + "setExtraHTTPHeaders", + None, + dict(headers=serialize_headers(headers)), + ) + + @property + def url(self) -> str: + return self._main_frame.url + + async def content(self, includeShadow: bool = None) -> str: + return await self._main_frame.content(includeShadow=includeShadow) + + async def set_content( + self, + html: str, + timeout: float = None, + waitUntil: DocumentLoadState = None, + ) -> None: + return await self._main_frame.set_content(**locals_to_params(locals())) + + async def goto( + self, + url: str, + timeout: float = None, + waitUntil: DocumentLoadState = None, + referer: str = None, + ) -> Optional[Response]: + return await self._main_frame.goto(**locals_to_params(locals())) + + async def reload( + self, + timeout: float = None, + waitUntil: DocumentLoadState = None, + ) -> Optional[Response]: + return from_nullable_channel( + await self._channel.send( + "reload", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), + ) + ) + + async def wait_for_load_state( + self, + state: Literal["domcontentloaded", "load", "networkidle"] = None, + timeout: float = None, + ) -> None: + return await self._main_frame.wait_for_load_state(**locals_to_params(locals())) + + async def wait_for_url( + self, + url: URLMatch, + waitUntil: DocumentLoadState = None, + timeout: float = None, + ) -> None: + return await self._main_frame.wait_for_url(**locals_to_params(locals())) + + async def wait_for_event( + self, event: str, predicate: Callable = None, timeout: float = None + ) -> Any: + async with self.expect_event(event, predicate, timeout) as event_info: + pass + return await event_info + + async def go_back( + self, + timeout: float = None, + waitUntil: DocumentLoadState = None, + ) -> Optional[Response]: + return from_nullable_channel( + await self._channel.send( + "goBack", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), + ) + ) + + async def go_forward( + self, + timeout: float = None, + waitUntil: DocumentLoadState = None, + ) -> Optional[Response]: + return from_nullable_channel( + await self._channel.send( + "goForward", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), + ) + ) + + async def request_gc(self) -> None: + await self._channel.send("requestGC", None) + + async def emulate_media( + self, + media: Literal["null", "print", "screen"] = None, + colorScheme: ColorScheme = None, + reducedMotion: ReducedMotion = None, + forcedColors: ForcedColors = None, + contrast: Contrast = None, + ) -> None: + params = locals_to_params(locals()) + if "media" in params: + params["media"] = "no-override" if params["media"] == "null" else media + if "colorScheme" in params: + params["colorScheme"] = ( + "no-override" if params["colorScheme"] == "null" else colorScheme + ) + if "reducedMotion" in params: + params["reducedMotion"] = ( + "no-override" if params["reducedMotion"] == "null" else reducedMotion + ) + if "forcedColors" in params: + params["forcedColors"] = ( + "no-override" if params["forcedColors"] == "null" else forcedColors + ) + if "contrast" in params: + params["contrast"] = ( + "no-override" if params["contrast"] == "null" else contrast + ) + await self._channel.send("emulateMedia", None, params) + + async def set_viewport_size(self, viewportSize: ViewportSize) -> None: + self._viewport_size = viewportSize + await self._channel.send( + "setViewportSize", + None, + locals_to_params(locals()), + ) + + @property + def viewport_size(self) -> Optional[ViewportSize]: + return self._viewport_size + + async def bring_to_front(self) -> None: + await self._channel.send("bringToFront", None) + + async def hide_highlight(self) -> None: + await self._channel.send("hideHighlight", None) + + async def add_init_script( + self, script: str = None, path: Union[str, Path] = None + ) -> Disposable: + if path: + script = add_source_url_to_script( + (await async_readfile(path)).decode(), path + ) + if not isinstance(script, str): + raise Error("Either path or script parameter must be specified") + return from_channel( + await self._channel.send("addInitScript", None, dict(source=script)) + ) + + async def route( + self, url: URLMatch, handler: RouteHandlerCallback, times: int = None + ) -> DisposableStub: + self._routes.insert( + 0, + RouteHandler( + self._browser_context._base_url, + url, + handler, + True if self._dispatcher_fiber else False, + times, + ), + ) + await self._update_interception_patterns() + return DisposableStub(lambda: self.unroute(url, handler), self) + + async def unroute( + self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None + ) -> None: + removed = [] + remaining = [] + for route in self._routes: + if route.url != url or (handler and route.handler != handler): + remaining.append(route) + else: + removed.append(route) + await self._unroute_internal(removed, remaining, "default") + + async def _unroute_internal( + self, + removed: List[RouteHandler], + remaining: List[RouteHandler], + behavior: Literal["default", "ignoreErrors", "wait"] = None, + ) -> None: + self._routes = remaining + if behavior is not None and behavior != "default": + await asyncio.gather( + *map( + lambda route: route.stop(behavior), # type: ignore + removed, + ) + ) + await self._update_interception_patterns() + + async def route_web_socket( + self, url: URLMatch, handler: WebSocketRouteHandlerCallback + ) -> None: + self._web_socket_routes.insert( + 0, + WebSocketRouteHandler(self._browser_context._base_url, url, handler), + ) + await self._update_web_socket_interception_patterns() + + def _dispose_har_routers(self) -> None: + for router in self._har_routers: + router.dispose() + self._har_routers = [] + + async def unroute_all( + self, behavior: Literal["default", "ignoreErrors", "wait"] = None + ) -> None: + await self._unroute_internal(self._routes, [], behavior) + self._dispose_har_routers() + + async def route_from_har( + self, + har: Union[Path, str], + url: Union[Pattern[str], str] = None, + notFound: RouteFromHarNotFoundPolicy = None, + update: bool = None, + updateContent: Literal["attach", "embed"] = None, + updateMode: HarMode = None, + ) -> None: + if update: + await self._browser_context._tracing._record_into_har( + har=har, + page=self, + url=url, + update_content=updateContent, + update_mode=updateMode, + ) + return + router = await HarRouter.create( + local_utils=self._connection.local_utils, + file=str(har), + not_found_action=notFound or "abort", + url_matcher=url, + ) + self._har_routers.append(router) + await router.add_page_route(self) + + async def _update_interception_patterns(self) -> None: + patterns = RouteHandler.prepare_interception_patterns(self._routes) + await self._channel.send( + "setNetworkInterceptionPatterns", + None, + {"patterns": patterns}, + ) + + async def _update_web_socket_interception_patterns(self) -> None: + patterns = WebSocketRouteHandler.prepare_interception_patterns( + self._web_socket_routes + ) + await self._channel.send( + "setWebSocketInterceptionPatterns", + None, + {"patterns": patterns}, + ) + + async def screenshot( + self, + timeout: float = None, + type: Literal["jpeg", "png", "webp"] = None, + path: Union[str, Path] = None, + quality: int = None, + omitBackground: bool = None, + fullPage: bool = None, + clip: FloatRect = None, + animations: Literal["allow", "disabled"] = None, + caret: Literal["hide", "initial"] = None, + scale: Literal["css", "device"] = None, + mask: Sequence["Locator"] = None, + maskColor: str = None, + style: str = None, + ) -> bytes: + params = locals_to_params(locals()) + if "path" in params: + if "type" not in params: + params["type"] = determine_screenshot_type(params["path"]) + del params["path"] + if "mask" in params: + params["mask"] = list( + map( + lambda locator: ( + { + "frame": locator._frame._channel, + "selector": locator._selector, + } + ), + params["mask"], + ) + ) + encoded_binary = await self._channel.send( + "screenshot", self._timeout_settings.timeout, params + ) + decoded_binary = base64.b64decode(encoded_binary) + if path: + make_dirs_for_file(path) + await async_writefile(path, decoded_binary) + return decoded_binary + + async def title(self) -> str: + return await self._main_frame.title() + + async def aria_snapshot( + self, + timeout: float = None, + depth: int = None, + mode: Literal["ai", "default"] = None, + boxes: bool = None, + ) -> str: + return await self._main_frame._channel.send( + "ariaSnapshot", + self._main_frame._timeout, + locals_to_params(locals()), + ) + + async def close(self, runBeforeUnload: bool = None, reason: str = None) -> None: + self._close_reason = reason + if not runBeforeUnload: + self._close_was_called = True + try: + if self._owned_context: + await self._owned_context.close() + elif runBeforeUnload: + await self._channel.send("runBeforeUnload", None) + else: + await self._channel.send("close", None, {"reason": reason}) + except Exception as e: + if is_target_closed_error(e) and not runBeforeUnload: + return + raise e + + def is_closed(self) -> bool: + return self._is_closed + + async def click( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + strict: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + return await self._main_frame._click(**locals_to_params(locals())) + + async def dblclick( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + return await self._main_frame.dblclick(**locals_to_params(locals())) + + async def tap( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + return await self._main_frame.tap(**locals_to_params(locals())) + + async def fill( + self, + selector: str, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + force: bool = None, + ) -> None: + return await self._main_frame.fill(**locals_to_params(locals())) + + def locator( + self, + selector: str, + hasText: Union[str, Pattern[str]] = None, + hasNotText: Union[str, Pattern[str]] = None, + has: "Locator" = None, + hasNot: "Locator" = None, + ) -> "Locator": + return self._main_frame.locator( + selector, + hasText=hasText, + hasNotText=hasNotText, + has=has, + hasNot=hasNot, + ) + + def get_by_alt_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self._main_frame.get_by_alt_text(text, exact=exact) + + def get_by_label( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self._main_frame.get_by_label(text, exact=exact) + + def get_by_placeholder( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self._main_frame.get_by_placeholder(text, exact=exact) + + def get_by_ref(self, ref: str) -> "Locator": + return self.locator(f"aria-ref={ref}") + + def get_by_role( + self, + role: AriaRole, + checked: bool = None, + disabled: bool = None, + expanded: bool = None, + includeHidden: bool = None, + level: int = None, + name: Union[str, Pattern[str]] = None, + pressed: bool = None, + selected: bool = None, + exact: bool = None, + description: Union[str, Pattern[str]] = None, + ) -> "Locator": + return self._main_frame.get_by_role( + role, + checked=checked, + disabled=disabled, + expanded=expanded, + includeHidden=includeHidden, + level=level, + name=name, + pressed=pressed, + selected=selected, + exact=exact, + description=description, + ) + + def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator": + return self._main_frame.get_by_test_id(testId) + + def get_by_text( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self._main_frame.get_by_text(text, exact=exact) + + def get_by_title( + self, text: Union[str, Pattern[str]], exact: bool = None + ) -> "Locator": + return self._main_frame.get_by_title(text, exact=exact) + + def frame_locator(self, selector: str = None) -> "FrameLocator": + return self.main_frame.frame_locator(selector) + + async def focus( + self, selector: str, strict: bool = None, timeout: float = None + ) -> None: + return await self._main_frame.focus(**locals_to_params(locals())) + + async def text_content( + self, selector: str, strict: bool = None, timeout: float = None + ) -> Optional[str]: + return await self._main_frame.text_content(**locals_to_params(locals())) + + async def inner_text( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: + return await self._main_frame.inner_text(**locals_to_params(locals())) + + async def inner_html( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: + return await self._main_frame.inner_html(**locals_to_params(locals())) + + async def get_attribute( + self, selector: str, name: str, strict: bool = None, timeout: float = None + ) -> Optional[str]: + return await self._main_frame.get_attribute(**locals_to_params(locals())) + + async def hover( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + return await self._main_frame.hover(**locals_to_params(locals())) + + async def drag_and_drop( + self, + source: str, + target: str, + sourcePosition: Position = None, + targetPosition: Position = None, + force: bool = None, + noWaitAfter: bool = None, + timeout: float = None, + strict: bool = None, + trial: bool = None, + steps: int = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + return await self._main_frame.drag_and_drop(**locals_to_params(locals())) + + async def select_option( + self, + selector: str, + value: Union[str, Sequence[str]] = None, + index: Union[int, Sequence[int]] = None, + label: Union[str, Sequence[str]] = None, + element: Union["ElementHandle", Sequence["ElementHandle"]] = None, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + strict: bool = None, + ) -> List[str]: + params = locals_to_params(locals()) + return await self._main_frame.select_option(**params) + + async def input_value( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: + params = locals_to_params(locals()) + return await self._main_frame.input_value(**params) + + async def set_input_files( + self, + selector: str, + files: Union[ + str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] + ], + timeout: float = None, + strict: bool = None, + noWaitAfter: bool = None, + ) -> None: + return await self._main_frame.set_input_files(**locals_to_params(locals())) + + async def type( + self, + selector: str, + text: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + ) -> None: + return await self._main_frame.type(**locals_to_params(locals())) + + async def press( + self, + selector: str, + key: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + ) -> None: + return await self._main_frame.press(**locals_to_params(locals())) + + async def check( + self, + selector: str, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + return await self._main_frame.check(**locals_to_params(locals())) + + async def uncheck( + self, + selector: str, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + return await self._main_frame.uncheck(**locals_to_params(locals())) + + async def wait_for_timeout(self, timeout: float) -> None: + await self._main_frame.wait_for_timeout(timeout) + + async def wait_for_function( + self, + expression: str, + arg: Serializable = None, + timeout: float = None, + polling: Union[float, Literal["raf"]] = None, + ) -> JSHandle: + return await self._main_frame.wait_for_function(**locals_to_params(locals())) + + @property + def workers(self) -> List["Worker"]: + return self._workers.copy() + + @property + def request(self) -> "APIRequestContext": + return self.context.request + + async def pause(self) -> None: + default_navigation_timeout = ( + self._browser_context._timeout_settings.default_navigation_timeout() + ) + default_timeout = self._browser_context._timeout_settings.default_timeout() + self._browser_context.set_default_navigation_timeout(0) + self._browser_context.set_default_timeout(0) + try: + await asyncio.wait( + [ + asyncio.create_task( + self._browser_context._channel.send("pause", None) + ), + self._closed_or_crashed_future, + ], + return_when=asyncio.FIRST_COMPLETED, + ) + finally: + self._browser_context._set_default_navigation_timeout_impl( + default_navigation_timeout + ) + self._browser_context._set_default_timeout_impl(default_timeout) + + async def pdf( + self, + scale: float = None, + displayHeaderFooter: bool = None, + headerTemplate: str = None, + footerTemplate: str = None, + printBackground: bool = None, + landscape: bool = None, + pageRanges: str = None, + format: str = None, + width: Union[str, float] = None, + height: Union[str, float] = None, + preferCSSPageSize: bool = None, + margin: PdfMargins = None, + path: Union[str, Path] = None, + outline: bool = None, + tagged: bool = None, + ) -> bytes: + params = locals_to_params(locals()) + if "path" in params: + del params["path"] + for key in ["width", "height"]: + if key in params: + params[key] = to_pdf_size(params[key]) + for key in ["top", "right", "bottom", "left"]: + if key in params.get("margin", {}): + params["margin"][key] = to_pdf_size(params["margin"][key]) + encoded_binary = await self._channel.send("pdf", None, params) + decoded_binary = base64.b64decode(encoded_binary) + if path: + make_dirs_for_file(path) + await async_writefile(path, decoded_binary) + return decoded_binary + + @property + def video(self) -> Optional[Video]: + # Video is only exposed when the page actually produced a recording artifact. + # The initializer carries the artifact; if absent, no video was recorded. + if not self._video._artifact: + return None + return self._video + + @property + def screencast(self) -> Screencast: + return self._screencast + + @property + def webmcp(self) -> WebMCP: + return self._main_frame.webmcp + + @property + def local_storage(self) -> WebStorage: + return self._local_storage + + @property + def session_storage(self) -> WebStorage: + return self._session_storage + + def _close_error_with_reason(self) -> TargetClosedError: + return TargetClosedError( + self._close_reason or self._browser_context._effective_close_reason() + ) + + def expect_event( + self, + event: str, + predicate: Callable = None, + timeout: float = None, + ) -> EventContextManagerImpl: + return self._expect_event( + event, predicate, timeout, f'waiting for event "{event}"' + ) + + def _expect_event( + self, + event: str, + predicate: Callable = None, + timeout: float = None, + log_line: str = None, + ) -> EventContextManagerImpl: + if timeout is None: + timeout = self._timeout_settings.timeout() + waiter = Waiter(self, f"page.expect_event({event})") + waiter.reject_on_timeout( + timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"' + ) + if log_line: + waiter.log(log_line) + if event != Page.Events.Crash: + waiter.reject_on_event(self, Page.Events.Crash, Error("Page crashed")) + if event != Page.Events.Close: + waiter.reject_on_event( + self, Page.Events.Close, lambda: self._close_error_with_reason() + ) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) + + def expect_console_message( + self, + predicate: Callable[[ConsoleMessage], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl[ConsoleMessage]: + return self.expect_event(Page.Events.Console, predicate, timeout) + + def expect_download( + self, + predicate: Callable[[Download], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl[Download]: + return self.expect_event(Page.Events.Download, predicate, timeout) + + def expect_file_chooser( + self, + predicate: Callable[[FileChooser], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl[FileChooser]: + return self.expect_event(Page.Events.FileChooser, predicate, timeout) + + def expect_navigation( + self, + url: URLMatch = None, + waitUntil: DocumentLoadState = None, + timeout: float = None, + ) -> EventContextManagerImpl[Response]: + return self.main_frame.expect_navigation(url, waitUntil, timeout) + + def expect_popup( + self, + predicate: Callable[["Page"], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl["Page"]: + return self.expect_event(Page.Events.Popup, predicate, timeout) + + def expect_request( + self, + urlOrPredicate: URLMatchRequest, + timeout: float = None, + ) -> EventContextManagerImpl[Request]: + def my_predicate(request: Request) -> Union[bool, Awaitable[bool]]: + if not callable(urlOrPredicate): + return url_matches( + self._browser_context._base_url, + request.url, + urlOrPredicate, + ) + return urlOrPredicate(request) + + trimmed_url = trim_url(urlOrPredicate) + log_line = f"waiting for request {trimmed_url}" if trimmed_url else None + return self._expect_event( + Page.Events.Request, + predicate=my_predicate, + timeout=timeout, + log_line=log_line, + ) + + def expect_request_finished( + self, + predicate: Callable[["Request"], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl[Request]: + return self.expect_event( + Page.Events.RequestFinished, predicate=predicate, timeout=timeout + ) + + def expect_response( + self, + urlOrPredicate: URLMatchResponse, + timeout: float = None, + ) -> EventContextManagerImpl[Response]: + def my_predicate(request: Response) -> Union[bool, Awaitable[bool]]: + if not callable(urlOrPredicate): + return url_matches( + self._browser_context._base_url, + request.url, + urlOrPredicate, + ) + return urlOrPredicate(request) + + trimmed_url = trim_url(urlOrPredicate) + log_line = f"waiting for response {trimmed_url}" if trimmed_url else None + return self._expect_event( + Page.Events.Response, + predicate=my_predicate, + timeout=timeout, + log_line=log_line, + ) + + def expect_websocket( + self, + predicate: Callable[["WebSocket"], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl["WebSocket"]: + return self.expect_event("websocket", predicate, timeout) + + def expect_worker( + self, + predicate: Callable[["Worker"], bool] = None, + timeout: float = None, + ) -> EventContextManagerImpl["Worker"]: + return self.expect_event("worker", predicate, timeout) + + async def set_checked( + self, + selector: str, + checked: bool, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + scroll: Literal["auto", "none"] = None, + ) -> None: + if checked: + await self.check( + selector=selector, + position=position, + timeout=timeout, + force=force, + strict=strict, + trial=trial, + scroll=scroll, + ) + else: + await self.uncheck( + selector=selector, + position=position, + timeout=timeout, + force=force, + strict=strict, + trial=trial, + scroll=scroll, + ) + + async def add_locator_handler( + self, + locator: "Locator", + handler: Union[Callable[["Locator"], Any], Callable[[], Any]], + noWaitAfter: bool = None, + times: int = None, + ) -> None: + if locator._frame != self._main_frame: + raise Error("Locator must belong to the main frame of this page") + if times == 0: + return + uid = await self._channel.send( + "registerLocatorHandler", + None, + { + "selector": locator._selector, + "noWaitAfter": noWaitAfter, + }, + ) + self._locator_handlers[uid] = LocatorHandler( + handler=handler, times=times, locator=locator + ) + + async def _on_locator_handler_triggered(self, uid: str) -> None: + remove = False + try: + handler = self._locator_handlers.get(uid) + if handler and handler.times != 0: + if handler.times is not None: + handler.times -= 1 + if self._dispatcher_fiber: + handler_finished_future = self._loop.create_future() + + def _handler() -> None: + try: + handler() + handler_finished_future.set_result(None) + except Exception as e: + handler_finished_future.set_exception(e) + + g = LocatorHandlerGreenlet(_handler) + g.switch() + await handler_finished_future + else: + coro_or_future = handler() + if coro_or_future: + await coro_or_future + remove = handler.times == 0 + finally: + if remove: + del self._locator_handlers[uid] + try: + await self._connection.wrap_api_call( + lambda: self._channel.send( + "resolveLocatorHandlerNoReply", + None, + {"uid": uid, "remove": remove}, + ), + is_internal=True, + ) + except Error: + pass + + async def remove_locator_handler(self, locator: "Locator") -> None: + for uid, data in self._locator_handlers.copy().items(): + if data.locator._equals(locator): + del self._locator_handlers[uid] + self._channel.send_no_reply( + "unregisterLocatorHandler", + None, + {"uid": uid}, + ) + + async def requests(self) -> List[Request]: + request_objects = await self._channel.send("requests", None) + return [from_channel(r) for r in request_objects] + + async def console_messages( + self, filter: Literal["all", "since-navigation"] = None + ) -> List[ConsoleMessage]: + message_dicts = await self._channel.send( + "consoleMessages", None, locals_to_params(locals()) + ) + return [ + ConsoleMessage( + {**event, "page": self._channel}, self._loop, self._dispatcher_fiber + ) + for event in message_dicts + ] + + async def page_errors( + self, filter: Literal["all", "since-navigation"] = None + ) -> List[Error]: + error_objects = await self._channel.send( + "pageErrors", None, locals_to_params(locals()) + ) + return [parse_error(error["error"]) for error in error_objects] + + async def clear_console_messages(self) -> None: + await self._channel.send("clearConsoleMessages", None) + + async def clear_page_errors(self) -> None: + await self._channel.send("clearPageErrors", None) + + async def pick_locator(self) -> "Locator": + selector = await self._channel.send("pickLocator", None, {}) + return self.locator(selector) + + async def cancel_pick_locator(self) -> None: + await self._channel.send("cancelPickLocator", None, {}) + + +class Worker(ChannelOwner): + Events = SimpleNamespace(Close="close", Console="console") + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._set_event_to_subscription_mapping({Worker.Events.Console: "console"}) + self._channel.on("close", lambda _: self._on_close()) + self._page: Optional[Page] = None + self._context: Optional["BrowserContext"] = None + + def __repr__(self) -> str: + return f"" + + def _on_close(self) -> None: + if self._page: + self._page._workers.remove(self) + if self._context: + self._context._service_workers.remove(self) + self.emit(Worker.Events.Close, self) + + @property + def url(self) -> str: + return self._initializer["url"] + + async def evaluate(self, expression: str, arg: Serializable = None) -> Any: + return parse_result( + await self._channel.send( + "evaluateExpression", + None, + dict( + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + async def evaluate_handle( + self, expression: str, arg: Serializable = None + ) -> JSHandle: + return from_channel( + await self._channel.send( + "evaluateExpressionHandle", + None, + dict( + expression=expression, + arg=serialize_argument(arg), + ), + ) + ) + + def expect_event( + self, + event: str, + predicate: Callable = None, + timeout: float = None, + ) -> EventContextManagerImpl: + if timeout is None: + if self._page: + timeout = self._page._timeout_settings.timeout() + elif self._context: + timeout = self._context._timeout_settings.timeout() + else: + timeout = 30000 + waiter = Waiter(self, f"worker.expect_event({event})") + waiter.reject_on_timeout( + cast(float, timeout), + f'Timeout {timeout}ms exceeded while waiting for event "{event}"', + ) + if event != Worker.Events.Close: + waiter.reject_on_event( + self, Worker.Events.Close, lambda: TargetClosedError() + ) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) + + +class BindingCall(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + + async def call(self, func: Callable) -> None: + try: + frame = from_channel(self._initializer["frame"]) + source = dict(context=frame._page.context, page=frame._page, frame=frame) + if self._initializer.get("handle"): + result = func(source, from_channel(self._initializer["handle"])) + else: + func_args = list(map(parse_result, self._initializer["args"])) + result = func(source, *func_args) + if inspect.iscoroutine(result): + result = await result + self._channel.send_may_fail( + "resolve", None, dict(result=serialize_argument(result)) + ) + except Exception as e: + tb = sys.exc_info()[2] + self._channel.send_may_fail( + "reject", None, dict(error=dict(error=serialize_error(e, tb))) + ) + + +def trim_url(param: Union[URLMatchRequest, URLMatchResponse]) -> Optional[str]: + if isinstance(param, re.Pattern): + return trim_end(param.pattern) + if isinstance(param, str): + return trim_end(param) + return None + + +def trim_end(s: str) -> str: + if len(s) > 50: + return s[:50] + "\u2026" + return s + + +def to_pdf_size(value: Any) -> Any: + # Unlabeled numbers are pixels, as in the JS client. bool is not a number here. + if isinstance(value, (int, float)) and not isinstance(value, bool): + return f"{value}px" + return value diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_path_utils.py b/venv/lib/python3.11/site-packages/playwright/_impl/_path_utils.py new file mode 100644 index 0000000..b405a06 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_path_utils.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from pathlib import Path +from types import FrameType +from typing import cast + + +def get_file_dirname() -> Path: + """Returns the callee (`__file__`) directory name""" + frame = cast(FrameType, inspect.currentframe()).f_back + module = inspect.getmodule(frame) + assert module + assert module.__file__ + return Path(module.__file__).parent.absolute() diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_playwright.py b/venv/lib/python3.11/site-packages/playwright/_impl/_playwright.py new file mode 100644 index 0000000..5c01511 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_playwright.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict + +from playwright._impl._browser_type import BrowserType +from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._fetch import APIRequest +from playwright._impl._selectors import Selectors + + +class Playwright(ChannelOwner): + devices: Dict + selectors: Selectors + chromium: BrowserType + firefox: BrowserType + webkit: BrowserType + request: APIRequest + + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self.request = APIRequest(self) + self.chromium = from_channel(initializer["chromium"]) + self.chromium._playwright = self + self.firefox = from_channel(initializer["firefox"]) + self.firefox._playwright = self + self.webkit = from_channel(initializer["webkit"]) + self.webkit._playwright = self + + self.selectors = Selectors(self._loop, self._dispatcher_fiber) + + self.devices = self._connection.local_utils.devices + + def __getitem__(self, value: str) -> "BrowserType": + if value == "chromium": + return self.chromium + elif value == "firefox": + return self.firefox + elif value == "webkit": + return self.webkit + raise ValueError("Invalid browser " + value) + + def _set_selectors(self, selectors: Selectors) -> None: + self.selectors = selectors + + async def stop(self) -> None: + pass diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_screencast.py b/venv/lib/python3.11/site-packages/playwright/_impl/_screencast.py new file mode 100644 index 0000000..2f39fc9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_screencast.py @@ -0,0 +1,184 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union + +from playwright._impl._api_structures import ( + ScreencastActionStyle, + ScreencastFrame, + ScreencastSize, +) +from playwright._impl._artifact import Artifact +from playwright._impl._connection import from_nullable_channel +from playwright._impl._disposable import DisposableStub +from playwright._impl._errors import Error +from playwright._impl._helper import locals_to_params + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + + +ScreencastFrameCallback = Callable[[ScreencastFrame], Any] +ScreencastPosition = Literal[ + "bottom", + "bottom-left", + "bottom-right", + "top", + "top-left", + "top-right", +] +ScreencastCursor = Literal[ + "none", + "pointer", +] + + +class Screencast: + def __init__(self, page: "Page") -> None: + self._page = page + self._loop = page._loop + self._dispatcher_fiber = page._dispatcher_fiber + self._started = False + self._save_path: Optional[Union[str, Path]] = None + self._on_frame: Optional[ScreencastFrameCallback] = None + self._artifact: Optional[Artifact] = None + page._channel.on("screencastFrame", self._dispatch_frame) + + def _ack_frame(self, frame_id: int) -> None: + try: + self._page._channel.send_no_reply( + "screencastFrameAck", None, {"frameId": frame_id} + ) + except (Error, OSError): + pass + + async def _await_callback_and_ack(self, result: Any, frame_id: int) -> None: + try: + await result + finally: + self._ack_frame(frame_id) + + def _dispatch_frame(self, params: dict) -> Any: + result = None + task = None + try: + if not self._on_frame: + return + data = params["data"] + if isinstance(data, str): + data = base64.b64decode(data) + result = self._on_frame( + { + "data": data, + "timestamp": params.get("timestamp", 0), + "viewportWidth": params["viewportWidth"], + "viewportHeight": params["viewportHeight"], + } + ) + finally: + if hasattr(result, "__await__"): + task = self._loop.create_task( + self._await_callback_and_ack(result, params["frameId"]) + ) + else: + self._ack_frame(params["frameId"]) + return task + + async def start( + self, + onFrame: ScreencastFrameCallback = None, + path: Union[str, Path] = None, + quality: int = None, + size: ScreencastSize = None, + fps: int = None, + ) -> DisposableStub: + if self._started: + raise Error("Screencast is already started") + self._started = True + self._on_frame = onFrame + result = await self._page._channel.send_return_as_dict( + "screencastStart", + None, + { + "size": size, + "quality": quality, + "fps": fps, + "sendFrames": bool(onFrame), + "record": bool(path), + }, + ) + artifact_channel = (result or {}).get("artifact") + if artifact_channel: + self._artifact = from_nullable_channel(artifact_channel) + self._save_path = path + return DisposableStub(lambda: self.stop(), self._page) + + async def stop(self) -> None: + self._started = False + self._on_frame = None + await self._page._channel.send("screencastStop", None) + if self._save_path and self._artifact: + await self._artifact.save_as(self._save_path) + self._artifact = None + self._save_path = None + + async def show_actions( + self, + duration: float = None, + position: ScreencastPosition = None, + fontSize: int = None, + cursor: ScreencastCursor = None, + style: ScreencastActionStyle = None, + ) -> DisposableStub: + await self._page._channel.send( + "screencastShowActions", None, locals_to_params(locals()) + ) + return DisposableStub(lambda: self.hide_actions(), self._page) + + async def hide_actions(self) -> None: + await self._page._channel.send("screencastHideActions", None) + + async def show_overlay(self, html: str, duration: float = None) -> DisposableStub: + result = await self._page._channel.send_return_as_dict( + "screencastShowOverlay", None, locals_to_params(locals()) + ) + overlay_id = (result or {}).get("id") + return DisposableStub( + lambda: self._page._channel.send( + "screencastRemoveOverlay", None, {"id": overlay_id} + ), + self._page, + ) + + async def show_chapter( + self, + title: str, + description: str = None, + duration: float = None, + ) -> None: + await self._page._channel.send( + "screencastChapter", None, locals_to_params(locals()) + ) + + async def show_overlays(self) -> None: + await self._page._channel.send( + "screencastSetOverlayVisible", None, {"visible": True} + ) + + async def hide_overlays(self) -> None: + await self._page._channel.send( + "screencastSetOverlayVisible", None, {"visible": False} + ) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_selectors.py b/venv/lib/python3.11/site-packages/playwright/_impl/_selectors.py new file mode 100644 index 0000000..c3bac78 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_selectors.py @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Union + +from playwright._impl._browser_context import BrowserContext +from playwright._impl._errors import Error +from playwright._impl._helper import async_readfile +from playwright._impl._locator import set_test_id_attribute_name + + +class Selectors: + def __init__(self, loop: asyncio.AbstractEventLoop, dispatcher_fiber: Any) -> None: + self._loop = loop + self._contexts_for_selectors: Set[BrowserContext] = set() + self._selector_engines: List[Dict] = [] + self._dispatcher_fiber = dispatcher_fiber + self._test_id_attribute_name: Optional[str] = None + + async def register( + self, + name: str, + script: str = None, + path: Union[str, Path] = None, + contentScript: bool = None, + ) -> None: + if any(engine for engine in self._selector_engines if engine["name"] == name): + raise Error( + f'Selectors.register: "{name}" selector engine has been already registered' + ) + if not script and not path: + raise Error("Either source or path should be specified") + if path: + script = (await async_readfile(path)).decode() + engine: Dict[str, Any] = dict(name=name, source=script) + if contentScript: + engine["contentScript"] = contentScript + for context in self._contexts_for_selectors: + await context._channel.send( + "registerSelectorEngine", + None, + {"selectorEngine": engine}, + ) + self._selector_engines.append(engine) + + def set_test_id_attribute(self, attributeName: str) -> None: + set_test_id_attribute_name(attributeName) + self._test_id_attribute_name = attributeName + for context in self._contexts_for_selectors: + context._channel.send_no_reply( + "setTestIdAttributeName", + None, + {"testIdAttributeName": attributeName}, + ) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_set_input_files_helpers.py b/venv/lib/python3.11/site-packages/playwright/_impl/_set_input_files_helpers.py new file mode 100644 index 0000000..65307e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_set_input_files_helpers.py @@ -0,0 +1,158 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import collections.abc +import os +import stat +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Dict, + List, + Optional, + Sequence, + Tuple, + TypedDict, + Union, + cast, +) + +from playwright._impl._connection import Channel, from_channel +from playwright._impl._helper import Error +from playwright._impl._writable_stream import WritableStream + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser_context import BrowserContext + +from playwright._impl._api_structures import FilePayload + +SIZE_LIMIT_IN_BYTES = 50 * 1024 * 1024 + + +class InputFilesList(TypedDict, total=False): + streams: Optional[List[Channel]] + directoryStream: Optional[Channel] + localDirectory: Optional[str] + localPaths: Optional[List[str]] + payloads: Optional[List[Dict[str, Union[str, bytes]]]] + + +def _list_files(directory: str) -> List[str]: + files = [] + for root, _, filenames in os.walk(directory): + for filename in filenames: + files.append(os.path.join(root, filename)) + return files + + +async def convert_input_files( + files: Union[ + str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] + ], + context: "BrowserContext", +) -> InputFilesList: + items = ( + files + if isinstance(files, collections.abc.Sequence) and not isinstance(files, str) + else [files] + ) + + if any([isinstance(item, (str, Path)) for item in items]): + if not all([isinstance(item, (str, Path)) for item in items]): + raise Error("File paths cannot be mixed with buffers") + + (local_paths, local_directory) = resolve_paths_and_directory_for_input_files( + cast(Sequence[Union[str, Path]], items) + ) + + if context._channel._connection.is_remote: + files_to_stream = cast( + List[str], + (_list_files(local_directory) if local_directory else local_paths), + ) + streams = [] + result = await context._connection.wrap_api_call( + lambda: context._channel.send_return_as_dict( + "createTempFiles", + None, + { + "rootDirName": ( + os.path.basename(local_directory) + if local_directory + else None + ), + "items": list( + map( + lambda file: dict( + name=( + os.path.relpath(file, local_directory) + if local_directory + else os.path.basename(file) + ), + lastModifiedMs=int(os.path.getmtime(file) * 1000), + ), + files_to_stream, + ) + ), + }, + ) + ) + for i, file in enumerate(result["writableStreams"]): + stream: WritableStream = from_channel(file) + await stream.copy(files_to_stream[i]) + streams.append(stream._channel) + return InputFilesList( + streams=None if local_directory else streams, + directoryStream=result.get("rootDir"), + ) + return InputFilesList(localPaths=local_paths, localDirectory=local_directory) + + file_payload_exceeds_size_limit = ( + sum([len(f.get("buffer", "")) for f in items if not isinstance(f, (str, Path))]) + > SIZE_LIMIT_IN_BYTES + ) + if file_payload_exceeds_size_limit: + raise Error( + "Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead." + ) + + return InputFilesList( + payloads=[ + { + "name": item["name"], + "mimeType": item["mimeType"], + "buffer": base64.b64encode(item["buffer"]).decode(), + } + for item in cast(List[FilePayload], items) + ] + ) + + +def resolve_paths_and_directory_for_input_files( + items: Sequence[Union[str, Path]], +) -> Tuple[Optional[List[str]], Optional[str]]: + local_paths: Optional[List[str]] = None + local_directory: Optional[str] = None + for item in items: + item_stat = os.stat(item) # Raises FileNotFoundError if doesn't exist + if stat.S_ISDIR(item_stat.st_mode): + if local_directory: + raise Error("Multiple directories are not supported") + local_directory = str(Path(item).resolve()) + else: + local_paths = local_paths or [] + local_paths.append(str(Path(item).resolve())) + if local_paths and local_directory: + raise Error("File paths must be all files or a single directory") + return (local_paths, local_directory) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_signature.py b/venv/lib/python3.11/site-packages/playwright/_impl/_signature.py new file mode 100644 index 0000000..adba677 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_signature.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import sys +from typing import Any, Callable + +if sys.version_info < (3, 14): + + def signature(fn: Callable[..., Any]) -> inspect.Signature: + return inspect.signature(fn) + +else: + # PEP 649 made annotations lazily evaluated, and inspect.signature() + # evaluates them eagerly by default. Handlers annotated with names that + # only exist under `if TYPE_CHECKING:` would raise NameError while all we + # need is the parameter list, so ask for unresolved forward references. + from annotationlib import Format + + def signature(fn: Callable[..., Any]) -> inspect.Signature: + return inspect.signature(fn, annotation_format=Format.FORWARDREF) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_str_utils.py b/venv/lib/python3.11/site-packages/playwright/_impl/_str_utils.py new file mode 100644 index 0000000..9fe6677 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_str_utils.py @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re +from typing import Pattern, Union + +from playwright._impl._errors import Error + + +def escape_regex_flags(pattern: Pattern) -> str: + flags = "" + if pattern.flags != 0: + flags = "" + if (pattern.flags & int(re.IGNORECASE)) != 0: + flags += "i" + if (pattern.flags & int(re.DOTALL)) != 0: + flags += "s" + if (pattern.flags & int(re.MULTILINE)) != 0: + flags += "m" + if ( + pattern.flags + & ~(int(re.MULTILINE) | int(re.IGNORECASE) | int(re.DOTALL) | int(re.UNICODE)) + != 0 + ): + raise Error( + "Unexpected re.Pattern flag, only MULTILINE, IGNORECASE and DOTALL are supported." + ) + return flags + + +def parse_regex_flags(flags: str) -> int: + result = 0 + if "i" in flags: + result |= re.IGNORECASE + if "s" in flags: + result |= re.DOTALL + if "m" in flags: + result |= re.MULTILINE + # The remaining JavaScript flags (d, g, u, v, y) have no re equivalent. + return result + + +def escape_for_regex(text: str) -> str: + return re.sub(r"[.*+?^>${}()|[\]\\]", "\\$&", text) + + +def escape_regex_for_selector(text: Pattern) -> str: + # Even number of backslashes followed by the quote -> insert a backslash. + return ( + "/" + + re.sub(r'(^|[^\\])(\\\\)*(["\'`])', r"\1\2\\\3", text.pattern).replace( + ">>", "\\>\\>" + ) + + "/" + + escape_regex_flags(text) + ) + + +def escape_for_text_selector( + text: Union[str, Pattern[str]], exact: bool = None, case_sensitive: bool = None +) -> str: + if isinstance(text, Pattern): + return escape_regex_for_selector(text) + return json.dumps(text) + ("s" if exact else "i") + + +def escape_for_attribute_selector( + value: Union[str, Pattern], exact: bool = None +) -> str: + if isinstance(value, Pattern): + return escape_regex_for_selector(value) + # TODO: this should actually be + # cssEscape(value).replace(/\\ /g, ' ') + # However, our attribute selectors do not conform to CSS parsing spec, + # so we escape them differently. + return ( + '"' + + value.replace("\\", "\\\\").replace('"', '\\"') + + '"' + + ("s" if exact else "i") + ) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_stream.py b/venv/lib/python3.11/site-packages/playwright/_impl/_stream.py new file mode 100644 index 0000000..ba0adb7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_stream.py @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +from pathlib import Path +from typing import Dict, Union + +from playwright._impl._connection import ChannelOwner + + +class Stream(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + + async def save_as(self, path: Union[str, Path]) -> None: + file = await self._loop.run_in_executor(None, lambda: open(path, "wb")) + try: + while True: + binary = await self._channel.send("read", None, {"size": 1024 * 1024}) + if not binary: + break + await self._loop.run_in_executor( + None, lambda: file.write(base64.b64decode(binary)) + ) + finally: + await self._loop.run_in_executor(None, lambda: file.close()) + + async def read_all(self) -> bytes: + binary = b"" + while True: + chunk = await self._channel.send("read", None, {"size": 1024 * 1024}) + if not chunk: + break + binary += base64.b64decode(chunk) + return binary diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_sync_base.py b/venv/lib/python3.11/site-packages/playwright/_impl/_sync_base.py new file mode 100644 index 0000000..35ddc73 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_sync_base.py @@ -0,0 +1,145 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import traceback +from contextlib import AbstractContextManager +from types import TracebackType +from typing import ( + Any, + Callable, + Coroutine, + Generator, + Generic, + Optional, + Type, + TypeVar, + Union, + cast, +) + +from playwright._impl._connection import _capture_stack_trace +from playwright._impl._greenlets import connection_closed_error, wait_for_future +from playwright._impl._helper import Error +from playwright._impl._impl_to_api_mapping import ImplToApiMapping, ImplWrapper + +mapping = ImplToApiMapping() + + +T = TypeVar("T") +Self = TypeVar("Self", bound="SyncContextManager") + + +class EventInfo(Generic[T]): + def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None: + self._sync_base = sync_base + self._future = future + + @property + def value(self) -> T: + wait_for_future( + self._sync_base._loop, self._sync_base._dispatcher_fiber, self._future + ) + exception = self._future.exception() + if exception: + raise exception + return cast(T, mapping.from_maybe_impl(self._future.result())) + + def _cancel(self) -> None: + self._future.cancel() + + def is_done(self) -> bool: + return self._future.done() + + +class EventContextManager(Generic[T], AbstractContextManager): + def __init__(self, sync_base: "SyncBase", future: "asyncio.Future[T]") -> None: + self._event = EventInfo[T](sync_base, future) + + def __enter__(self) -> EventInfo[T]: + return self._event + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + if exc_val: + self._event._cancel() + else: + self._event.value + + +class SyncBase(ImplWrapper): + def __init__(self, impl_obj: Any) -> None: + super().__init__(impl_obj) + self._loop: asyncio.AbstractEventLoop = impl_obj._loop + self._dispatcher_fiber = impl_obj._dispatcher_fiber + + def __str__(self) -> str: + return self._impl_obj.__str__() + + def _sync( + self, + coro: Union[Coroutine[Any, Any, Any], Generator[Any, Any, Any]], + ) -> Any: + __tracebackhide__ = True + if self._loop.is_closed(): + coro.close() + raise Error("Event loop is closed! Is Playwright already stopped?") + if self._dispatcher_fiber.dead: + coro.close() + raise connection_closed_error() + + task: asyncio.tasks.Task[Any] = self._loop.create_task(coro) + setattr(task, "__pw_stack__", _capture_stack_trace()) + setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)) + + wait_for_future(self._loop, self._dispatcher_fiber, task) + return task.result() + + def _wrap_handler(self, handler: Union[Callable[..., Any], Any]) -> Any: + if callable(handler): + return mapping.wrap_handler(handler) + return handler + + def on(self, event: Any, f: Any) -> None: + """Registers the function ``f`` to the event name ``event``.""" + self._impl_obj.on(event, self._wrap_handler(f)) + + def once(self, event: Any, f: Any) -> None: + """The same as ``self.on``, except that the listener is automatically + removed after being called. + """ + self._impl_obj.once(event, self._wrap_handler(f)) + + def remove_listener(self, event: Any, f: Any) -> None: + """Removes the function ``f`` from ``event``.""" + self._impl_obj.remove_listener(event, self._wrap_handler(f)) + + +class SyncContextManager(SyncBase): + def __enter__(self: Self) -> Self: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + _traceback: Optional[TracebackType], + ) -> None: + self.close() + + def close(self) -> None: ... diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_tracing.py b/venv/lib/python3.11/site-packages/playwright/_impl/_tracing.py new file mode 100644 index 0000000..93ff31c --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_tracing.py @@ -0,0 +1,287 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +from typing import Any, Dict, Literal, Optional, Pattern, Union, cast + +from playwright._impl._api_structures import TracingGroupLocation +from playwright._impl._artifact import Artifact +from playwright._impl._connection import ( + ChannelOwner, + from_channel, + from_nullable_channel, +) +from playwright._impl._disposable import DisposableStub +from playwright._impl._helper import Error, locals_to_params + + +class Tracing(ChannelOwner): + def __init__( + self, parent: ChannelOwner, type: str, guid: str, initializer: Dict + ) -> None: + super().__init__(parent, type, guid, initializer) + self._include_sources: bool = False + self._is_live: bool = False + self._stacks_id: Optional[str] = None + self._is_tracing: bool = False + self._traces_dir: Optional[str] = None + self._har_id: Optional[str] = None + self._har_recorders: Dict[str, Dict[str, str]] = {} + + async def start( + self, + name: str = None, + title: str = None, + snapshots: bool = None, + ariaSnapshots: bool = None, + screenSnapshots: bool = None, + screenshots: bool = None, + sources: bool = None, + live: bool = None, + ) -> DisposableStub: + params = locals_to_params(locals()) + self._include_sources = bool(sources) + self._is_live = bool(live) + + await self._channel.send( + "tracingStart", + None, + { + "name": name, + "snapshotDom": snapshots, + "snapshotAria": ariaSnapshots, + "snapshotScreen": screenSnapshots, + "screencast": screenshots, + "live": live, + }, + ) + trace_name = await self._channel.send( + "tracingStartChunk", None, {"title": title, "name": name} + ) + await self._start_collecting_stacks(trace_name) + return DisposableStub(lambda: self.stop(), self) + + async def start_chunk(self, title: str = None, name: str = None) -> DisposableStub: + params = locals_to_params(locals()) + trace_name = await self._channel.send("tracingStartChunk", None, params) + await self._start_collecting_stacks(trace_name) + return DisposableStub(lambda: self.stop_chunk(), self) + + async def _start_collecting_stacks(self, trace_name: str) -> None: + if not self._is_tracing: + self._is_tracing = True + self._connection.set_is_tracing(True) + self._stacks_id = await self._connection.local_utils.tracing_started( + self._traces_dir, trace_name, self._is_live + ) + + async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None: + await self._do_stop_chunk(path) + + async def stop(self, path: Union[pathlib.Path, str] = None) -> None: + await self._do_stop_chunk(path) + await self._channel.send( + "tracingStop", + None, + ) + + async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> None: + self._reset_stack_counter() + + if not file_path: + # Not interested in any artifacts + await self._channel.send("tracingStopChunk", None, {"mode": "discard"}) + if self._stacks_id: + await self._connection.local_utils.trace_discarded(self._stacks_id) + return + + is_local = not self._connection.is_remote + + if is_local: + result = await self._channel.send_return_as_dict( + "tracingStopChunk", None, {"mode": "entries"} + ) + await self._connection.local_utils.zip( + { + "zipFile": str(file_path), + "entries": result["entries"], + "stacksId": self._stacks_id, + "mode": "write", + "includeSources": self._include_sources, + } + ) + return + + result = await self._channel.send_return_as_dict( + "tracingStopChunk", + None, + { + "mode": "archive", + }, + ) + + artifact = cast( + Optional[Artifact], + from_nullable_channel(result.get("artifact")), + ) + + # The artifact may be missing if the browser closed while stopping tracing. + if not artifact: + if self._stacks_id: + await self._connection.local_utils.trace_discarded(self._stacks_id) + return + + # Save trace to the final local file. + await artifact.save_as(file_path) + await artifact.delete() + + await self._connection.local_utils.zip( + { + "zipFile": str(file_path), + "entries": [], + "stacksId": self._stacks_id, + "mode": "append", + "includeSources": self._include_sources, + } + ) + + def _reset_stack_counter(self) -> None: + if self._is_tracing: + self._is_tracing = False + self._connection.set_is_tracing(False) + + async def group( + self, name: str, location: TracingGroupLocation = None + ) -> DisposableStub: + await self._channel.send("tracingGroup", None, locals_to_params(locals())) + return DisposableStub(lambda: self.group_end(), self) + + async def group_end(self) -> None: + await self._channel.send( + "tracingGroupEnd", + None, + ) + + async def start_har( + self, + path: Union[pathlib.Path, str], + content: Literal["attach", "embed", "omit"] = None, + mode: Literal["full", "minimal"] = None, + urlFilter: Union[str, Pattern[str]] = None, + ) -> DisposableStub: + if self._har_id: + raise Error("HAR recording has already been started") + is_zip = str(path).endswith(".zip") + default_content: Literal["attach", "embed", "omit"] = ( + "attach" if is_zip else "embed" + ) + self._har_id = await self._record_into_har( + har=path, + page=None, + url=urlFilter, + update_content=content or default_content, + update_mode=mode or "full", + ) + return DisposableStub(lambda: self.stop_har(), self) + + async def _record_into_har( + self, + har: Union[pathlib.Path, str], + page: Optional[ChannelOwner], + url: Union[str, Pattern[str]] = None, + update_content: Literal["attach", "embed", "omit"] = None, + update_mode: Literal["full", "minimal"] = None, + resourcesDir: Optional[str] = None, + ) -> str: + is_zip = str(har).endswith(".zip") + url_glob: Optional[str] = None + url_regex_source: Optional[str] = None + url_regex_flags: Optional[str] = None + if isinstance(url, str): + url_glob = url + elif url is not None: + url_regex_source = url.pattern + url_regex_flags = "".join( + flag + for flag, mask in (("i", 2), ("m", 8), ("s", 16)) + if url.flags & mask + ) + options: Dict[str, object] = { + "content": update_content or "attach", + "mode": update_mode or "minimal", + "harPath": None if is_zip else str(har), + } + if url_glob is not None: + options["urlGlob"] = url_glob + if url_regex_source is not None: + options["urlRegexSource"] = url_regex_source + if url_regex_flags is not None: + options["urlRegexFlags"] = url_regex_flags + if resourcesDir is not None: + options["resourcesDir"] = resourcesDir + params: Dict[str, Any] = {"options": options} + if page is not None: + params["page"] = page._channel + result = await self._channel.send_return_as_dict("harStart", None, params) + har_id = result["harId"] + self._har_recorders[har_id] = {"path": str(har)} + return har_id + + async def _export_all_hars(self) -> None: + for har_id in list(self._har_recorders.keys()): + await self._export_har(har_id) + self._har_id = None + + async def stop_har(self) -> None: + har_id = self._har_id + if not har_id: + return + self._har_id = None + await self._export_har(har_id) + + async def _export_har(self, har_id: str) -> None: + params = self._har_recorders.pop(har_id, None) + if not params: + return + is_local = not self._connection.is_remote + is_zip = params["path"].endswith(".zip") + + if is_local: + result = await self._channel.send_return_as_dict( + "harExport", None, {"harId": har_id, "mode": "entries"} + ) + if not is_zip: + # Server wrote HAR and resources to the user's chosen paths. + return + await self._connection.local_utils.zip( + { + "zipFile": params["path"], + "entries": result["entries"], + "mode": "write", + "includeSources": False, + } + ) + return + + result = await self._channel.send_return_as_dict( + "harExport", None, {"harId": har_id, "mode": "archive"} + ) + artifact = cast(Artifact, from_channel(result["artifact"])) + if is_zip: + await artifact.save_as(params["path"]) + await artifact.delete() + return + # Uncompressed har is not supported in thin clients + await artifact.save_as(params["path"] + ".tmp") + await artifact.delete() diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_transport.py b/venv/lib/python3.11/site-packages/playwright/_impl/_transport.py new file mode 100644 index 0000000..5b9557e --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_transport.py @@ -0,0 +1,189 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import io +import json +import os +import subprocess +import sys +from abc import ABC, abstractmethod +from typing import Callable, Dict, Optional, Union + +from playwright._impl._driver import compute_driver_executable, get_driver_env +from playwright._impl._errors import Error +from playwright._impl._helper import ParsedMessagePayload + + +# Sourced from: https://github.com/pytest-dev/pytest/blob/da01ee0a4bb0af780167ecd228ab3ad249511302/src/_pytest/faulthandler.py#L69-L77 +def _get_stderr_fileno() -> Optional[int]: + try: + # when using pythonw, sys.stderr is None. + # when Pyinstaller is used, there is no closed attribute because Pyinstaller monkey-patches it with a NullWriter class + if sys.stderr is None or not hasattr(sys.stderr, "closed"): + return None + if sys.stderr.closed: + return None + + return sys.stderr.fileno() + except (NotImplementedError, AttributeError, io.UnsupportedOperation): + # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. + # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors + # This is potentially dangerous, but the best we can do. + if not hasattr(sys, "__stderr__") or not sys.__stderr__: + return None + return sys.__stderr__.fileno() + + +class Transport(ABC): + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + self._loop = loop + self.on_message: Callable[[ParsedMessagePayload], None] = lambda _: None + self.on_error_future: asyncio.Future = loop.create_future() + + @abstractmethod + def request_stop(self) -> None: + pass + + def dispose(self) -> None: + pass + + @abstractmethod + async def wait_until_stopped(self) -> None: + pass + + @abstractmethod + async def connect(self) -> None: + pass + + @abstractmethod + async def run(self) -> None: + pass + + @abstractmethod + def send(self, message: Dict) -> None: + pass + + def serialize_message(self, message: Dict) -> bytes: + try: + # NaN and Infinity are not valid JSON and would crash the driver. + msg = json.dumps(message, allow_nan=False) + except ValueError as e: + raise Error(str(e)) from None + if "DEBUGP" in os.environ: # pragma: no cover + print("\x1b[32mSEND>\x1b[0m", json.dumps(message, indent=2)) + return msg.encode() + + def deserialize_message(self, data: Union[str, bytes]) -> ParsedMessagePayload: + obj = json.loads(data) + + if "DEBUGP" in os.environ: # pragma: no cover + print("\x1b[33mRECV>\x1b[0m", json.dumps(obj, indent=2)) + return obj + + +class PipeTransport(Transport): + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + super().__init__(loop) + self._stopped = False + + def request_stop(self) -> None: + assert self._output + self._stopped = True + self._output.close() + + async def wait_until_stopped(self) -> None: + await self._stopped_future + + async def connect(self) -> None: + self._stopped_future: asyncio.Future = asyncio.Future() + + try: + # For pyinstaller and Nuitka + env = get_driver_env() + if getattr(sys, "frozen", False) or globals().get("__compiled__"): + env.setdefault("PLAYWRIGHT_BROWSERS_PATH", "0") + + startupinfo = None + if sys.platform == "win32": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + + executable_path, entrypoint_path = compute_driver_executable() + self._proc = await asyncio.create_subprocess_exec( + executable_path, + entrypoint_path, + "run-driver", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=_get_stderr_fileno(), + limit=32768, + env=env, + startupinfo=startupinfo, + ) + except Exception as exc: + self.on_error_future.set_exception(exc) + raise exc + + self._output = self._proc.stdin + + async def run(self) -> None: + assert self._proc.stdout + assert self._proc.stdin + try: + while not self._stopped: + try: + buffer = await self._proc.stdout.readexactly(4) + if self._stopped: + break + length = int.from_bytes(buffer, byteorder="little", signed=False) + buffer = bytes(0) + while length: + to_read = min(length, 32768) + data = await self._proc.stdout.readexactly(to_read) + if self._stopped: + break + length -= to_read + if len(buffer): + buffer = buffer + data + else: + buffer = data + if self._stopped: + break + + obj = self.deserialize_message(buffer) + self.on_message(obj) + except asyncio.IncompleteReadError: + if not self._stopped: + self.on_error_future.set_exception( + Exception("Connection closed while reading from the driver") + ) + break + await asyncio.sleep(0) + + await self._proc.communicate() + finally: + # Release waiters on wait_until_stopped() even if this task was + # cancelled before reaching the end (e.g. by asyncio.run()'s + # task-cancellation phase that runs before asyncio-atexit hooks). + if not self._stopped_future.done(): + self._stopped_future.set_result(None) + + def send(self, message: Dict) -> None: + assert self._output + data = self.serialize_message(message) + self._output.write( + len(data).to_bytes(4, byteorder="little", signed=False) + data + ) diff --git a/venv/lib/python3.11/site-packages/playwright/_impl/_video.py b/venv/lib/python3.11/site-packages/playwright/_impl/_video.py new file mode 100644 index 0000000..0d64866 --- /dev/null +++ b/venv/lib/python3.11/site-packages/playwright/_impl/_video.py @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +from typing import TYPE_CHECKING, Optional, Union + +from playwright._impl._artifact import Artifact +from playwright._impl._helper import Error + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._page import Page + + +class Video: + def __init__(self, page: "Page", artifact: Optional[Artifact]) -> None: + self._loop = page._loop + self._dispatcher_fiber = page._dispatcher_fiber + self._page = page + self._is_remote = page._connection.is_remote + self._artifact = artifact + + def __repr__(self) -> str: + return f"