diff --git a/pyproject.toml b/pyproject.toml index 3b6237d8..96abb5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ dev = [ "ruff>=0.2.0", "robotframework-robocop>=6", "lxml>=4.9.3", - "lxml-stubs>=0.4.0", "pytest-xdist>=3.5.0", "pytest-github-actions-annotate-failures>=0.2.0", "robotcode[analyze]>=0.97.0", @@ -43,6 +42,7 @@ dev = [ # versions. see https://github.com/astral-sh/uv/issues/16650#issuecomment-3508516822 "pytest>=7a0", "robotframework>=6.1a0", + "types-lxml>=2026.2.16", ] [project.urls] diff --git a/tests/conftest.py b/tests/conftest.py index e283aa55..cc66f714 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,10 +18,11 @@ from os import PathLike from _typeshed import StrPath - from lxml.etree import ( - _AnyStr, # pyright: ignore[reportPrivateUsage] - _NonDefaultNSMapArg, # pyright: ignore[reportPrivateUsage] - _XPathObject, # pyright: ignore[reportPrivateUsage] + from lxml._types import ( # pyright:ignore[reportMissingModuleSource] https://github.com/DetachHead/basedpyright/issues/615 + _TextArg, # pyright: ignore[reportPrivateUsage] + _XPathExtFuncArg, # pyright: ignore[reportPrivateUsage] + _XPathNSArg, # pyright: ignore[reportPrivateUsage] + _XPathVarArg, # pyright: ignore[reportPrivateUsage] ) from typing_extensions import Never @@ -133,7 +134,8 @@ def _is_dunder(name: str) -> bool: return len(name) > 4 and name[:2] == name[-2:] == "__" and name[2] != "_" and name[-3] != "_" -def _is_element_list(xpath_object: _XPathObject) -> TypeGuard[list[_Element]]: +# ideally should be _XPathObject, see https://github.com/abelcheung/types-lxml/issues/125 +def _is_element_list(xpath_object: object) -> TypeGuard[list[_Element]]: if isinstance(xpath_object, list): if xpath_object: return isinstance(xpath_object[0], _Element) @@ -149,7 +151,7 @@ def __init__(self, element: _Element) -> None: @override def __getattribute__(self, /, name: str) -> object: - if _is_dunder(name) or name not in vars(_Element): + if _is_dunder(name) or name in vars(_XmlElement) or name in vars(self): return super().__getattribute__(name) # pyright:ignore[reportAny] return getattr(self._proxied, name) # pyright:ignore[reportAny] @@ -169,17 +171,28 @@ def __iter__(self) -> Iterator[_XmlElement]: def xpath( self, - _path: _AnyStr, - namespaces: _NonDefaultNSMapArg | None = None, - extensions: object = ..., + _path: _TextArg, + namespaces: _XPathNSArg | None = None, + extensions: _XPathExtFuncArg | None = None, *, smart_strings: bool = True, - **_variables: _XPathObject, - ) -> _XPathObject: - result = self._proxied.xpath(_path, namespaces, extensions, smart_strings, **_variables) + **_variables: _XPathVarArg, + ): + # https://github.com/abelcheung/types-lxml/issues/125 + result = cast( + object, + self._proxied.xpath( + _path, + namespaces=namespaces, + extensions=extensions, + smart_strings=smart_strings, + **_variables, + ), + ) if _is_element_list(result): - # variance moment, but we aren't storing the value anywhere so it's fine - return [_XmlElement(element) for element in result] # pyright:ignore[reportReturnType] # ty:ignore[invalid-return-type] + # once the issue linked above resolved, a variance related type error will probably + # appear here, but we aren't storing the value anywhere it's fine to ignore. + return [_XmlElement(element) for element in result] return result def count_children(self) -> int: @@ -194,13 +207,14 @@ class XmlElement(_Element): behavior """ - def __init__(self, element: _Element) -> None: ... + # https://github.com/DetachHead/basedpyright/issues/615 + def __init__(self, element: _Element) -> None: ... # pyright:ignore[reportMissingSuperCall] - def __bool__(self) -> Literal[True]: # pyright:ignore[reportReturnType] + def __bool__(self) -> Literal[True]: # pyright:ignore[reportReturnType] see issue above """normally this returns `True` only if it has children""" @override - def __len__(self) -> Never: # pyright:ignore[reportReturnType] + def __len__(self) -> Never: # pyright:ignore[reportReturnType] see issue above """ normally this returns how many children it has. but if you want to check than then call `count_children` instead @@ -217,11 +231,12 @@ def output_xml() -> XmlElement: def xpath(xml: _Element, query: str) -> XmlElement: - results = xml.xpath(query) + # https://github.com/abelcheung/types-lxml/issues/125 + results = cast(object, xml.xpath(query)) assert isinstance(results, list) - (result,) = results - assert isinstance(result, _Element) - return XmlElement(result) + (result,) = results # pyright:ignore[reportUnknownVariableType] see issue above + assert isinstance(result, XmlElement) + return result def assert_robot_total_stats(*, passed: int = 0, skipped: int = 0, failed: int = 0): diff --git a/tests/test_python.py b/tests/test_python.py index df1b6d4f..00b353b2 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -275,7 +275,7 @@ def test_error_moment_exitonerror_multiple_tests(pr: PytestRobotTester): ) assert ( bool( - xml.xpath( + xml.xpath( # pyright:ignore[reportAny] https://github.com/abelcheung/types-lxml/issues/125 ".//test[@name='test_bar']/status[@status='FAIL' and .='Error occurred" " and exit-on-error mode is in use.']" ) diff --git a/tests/test_robot.py b/tests/test_robot.py index 33cc6879..0587051f 100644 --- a/tests/test_robot.py +++ b/tests/test_robot.py @@ -148,7 +148,7 @@ def test_tags(pr: PytestRobotTester): def test_tags_in_settings(pr: PytestRobotTester): - pr.run_and_assert_result("-m", "m1", passed=2) + pr.run_and_assert_result("-m", "m1", passed=2, subprocess=True) pr.assert_log_file_exists() xml = output_xml() assert xml.xpath(".//test[@name='Foo']/tag[.='m1']") @@ -218,9 +218,7 @@ def test_run_keyword_and_ignore_error(pr: PytestRobotTester): def test_init_file(pr: PytestRobotTester): pr.run_and_assert_result(passed=1) pr.assert_log_file_exists() - assert cast(str, xpath(output_xml(), "/robot/suite").attrib["name"]).startswith( - "Test Init File" - ) + assert xpath(output_xml(), "/robot/suite").attrib["name"].startswith("Test Init File") def test_init_file_nested(pr: PytestRobotTester): @@ -347,6 +345,9 @@ def test_pass_execution(pr: PytestRobotTester): def test_keyword_called_twice_in_robot_test(pr: PytestRobotTester): pr.run_and_assert_result(passed=1) pr.assert_log_file_exists() - results = output_xml().xpath("//kw[@name='Run Test']/kw[@name='Bar']/kw[@name='Foo']") + # https://github.com/abelcheung/types-lxml/issues/125 + results = cast( + object, output_xml().xpath("//kw[@name='Run Test']/kw[@name='Bar']/kw[@name='Foo']") + ) assert isinstance(results, list) - assert len(results) == 2 + assert len(results) == 2 # pyright:ignore[reportUnknownArgumentType] see issue above diff --git a/uv.lock b/uv.lock index cea7f664..536fa481 100644 --- a/uv.lock +++ b/uv.lock @@ -228,6 +228,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cssselect" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/2e/cdfd8b01c37cbf4f9482eefd455853a3cf9c995029a46acd31dfaa9c1dd6/cssselect-1.4.0.tar.gz", hash = "sha256:fdaf0a1425e17dfe8c5cf66191d211b357cf7872ae8afc4c6762ddd8ac47fc92", size = 40589, upload-time = "2026-01-29T07:00:26.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/0c/7bb51e3acfafd16c48875bf3db03607674df16f5b6ef8d056586af7e2b8b/cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8", size = 18540, upload-time = "2026-01-29T07:00:24.994Z" }, +] + [[package]] name = "dill" version = "0.4.1" @@ -484,15 +493,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, ] -[[package]] -name = "lxml-stubs" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -1026,7 +1026,6 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, { name = "tomli", version = "2.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_version < '0'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ @@ -1063,7 +1062,6 @@ dev = [ { name = "exceptiongroup" }, { name = "griffe-public-redundant-aliases" }, { name = "lxml" }, - { name = "lxml-stubs" }, { name = "mike" }, { name = "mkdocs-awesome-pages-plugin" }, { name = "mkdocs-material" }, @@ -1079,6 +1077,7 @@ dev = [ { name = "ruff" }, { name = "tach" }, { name = "ty" }, + { name = "types-lxml" }, ] [package.metadata] @@ -1096,7 +1095,6 @@ dev = [ { name = "exceptiongroup", specifier = ">=1.3.0" }, { name = "griffe-public-redundant-aliases", specifier = ">=0.3.0" }, { name = "lxml", specifier = ">=4.9.3" }, - { name = "lxml-stubs", specifier = ">=0.4.0" }, { name = "mike", specifier = ">=2.1.3" }, { name = "mkdocs-awesome-pages-plugin", specifier = ">=2.10.1" }, { name = "mkdocs-material", specifier = ">=9.6.17" }, @@ -1112,6 +1110,7 @@ dev = [ { name = "ruff", specifier = ">=0.2.0" }, { name = "tach", specifier = ">=0.32.2" }, { name = "ty", specifier = ">=0.0.0a8" }, + { name = "types-lxml", specifier = ">=2026.2.16" }, ] [[package]] @@ -1614,6 +1613,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, ] +[[package]] +name = "types-html5lib" +version = "1.1.11.20260508" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/d7/8512bdd1d3102a12d5125ad0850153c795bd99f4f5ef496498e494912787/types_html5lib-1.1.11.20260508.tar.gz", hash = "sha256:f44f55f3bd1f7a55477851165c8955a821f2342447a995a17f10a376134ea049", size = 18377, upload-time = "2026-05-08T04:51:35.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/e5/f1c8f553b392c447fbab985e100cf3998e657c10b5996de88f2815b09234/types_html5lib-1.1.11.20260508-py3-none-any.whl", hash = "sha256:cb85cb9dd46cdb18349f9ad08df69cc911921849e154860dad9029da4a2c1c39", size = 24312, upload-time = "2026-05-08T04:51:34.383Z" }, +] + +[[package]] +name = "types-lxml" +version = "2026.2.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "cssselect" }, + { name = "types-html5lib" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/ad/c70ac8cbdc28eb58a17301c69b4925af54b614e47f9b2ebc9de5cc10f786/types_lxml-2026.2.16.tar.gz", hash = "sha256:b3a1340cc06db98d541c785732f6f68bea438daff4e2b7809ef748d545d01406", size = 161204, upload-time = "2026-02-17T02:34:50.855Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5c/03ec9befbf4bb5309bfd576c6a5ac1c75633f78f6b64cf1f594e97cd3d23/types_lxml-2026.2.16-py3-none-any.whl", hash = "sha256:5dd81ffa54830e5f361988737c5f1d6a0ae48b2742790637ec560df790ea0401", size = 97040, upload-time = "2026-02-17T02:34:49.286Z" }, +] + +[[package]] +name = "types-webencodings" +version = "0.5.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/d2/21567fac142315580ce3ee37d08a4e8819921dae833bbcd27b9f8b373799/types_webencodings-0.5.0.20260408.tar.gz", hash = "sha256:28c596619f367e43eee393d85f63e8d2fdb6874c654a8d441c37f8afe29c6d0d", size = 7504, upload-time = "2026-04-08T04:28:51.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/e4/f13be8f6d9a561166f7d963012d0ccc833e13aee3044c4f1a8fb1fee462a/types_webencodings-0.5.0.20260408-py3-none-any.whl", hash = "sha256:19a2afe5c22d9b1e880b49ff823c7b531f473a390fe47ac903c0bdb5cd677dd9", size = 8717, upload-time = "2026-04-08T04:28:50.943Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"