Skip to content

Commit 0bb0e34

Browse files
authored
feat(docs): validate doc parameters, report errors (#104)
1 parent edc6a6f commit 0bb0e34

10 files changed

+1137
-1300
lines changed

playwright/async_api.py

+479-619
Large diffs are not rendered by default.

playwright/browser_context.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -158,20 +158,19 @@ async def exposeBinding(self, name: str, binding: FunctionWithSource) -> None:
158158
async def exposeFunction(self, name: str, binding: Callable[..., Any]) -> None:
159159
await self.exposeBinding(name, lambda source, *args: binding(*args))
160160

161-
async def route(self, match: URLMatch, handler: RouteHandler) -> None:
162-
self._routes.append(RouteHandlerEntry(URLMatcher(match), handler))
161+
async def route(self, url: URLMatch, handler: RouteHandler) -> None:
162+
self._routes.append(RouteHandlerEntry(URLMatcher(url), handler))
163163
if len(self._routes) == 1:
164164
await self._channel.send(
165165
"setNetworkInterceptionEnabled", dict(enabled=True)
166166
)
167167

168168
async def unroute(
169-
self, match: URLMatch, handler: Optional[RouteHandler] = None
169+
self, url: URLMatch, handler: Optional[RouteHandler] = None
170170
) -> None:
171171
self._routes = list(
172172
filter(
173-
lambda r: r.matcher.match != match
174-
or (handler and r.handler != handler),
173+
lambda r: r.matcher.match != url or (handler and r.handler != handler),
175174
self._routes,
176175
)
177176
)

playwright/dialog.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def message(self) -> str:
3434
def defaultValue(self) -> str:
3535
return self._initializer["defaultValue"]
3636

37-
async def accept(self, prompt_text: str = None) -> None:
37+
async def accept(self, promptText: str = None) -> None:
3838
await self._channel.send("accept", locals_to_params(locals()))
3939

4040
async def dismiss(self) -> None:

playwright/js_handle.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ async def evaluateHandle(
6969
)
7070
)
7171

72-
async def getProperty(self, name: str) -> "JSHandle":
73-
return from_channel(await self._channel.send("getProperty", dict(name=name)))
72+
async def getProperty(self, propertyName: str) -> "JSHandle":
73+
return from_channel(
74+
await self._channel.send("getProperty", dict(name=propertyName))
75+
)
7476

7577
async def getProperties(self) -> Dict[str, "JSHandle"]:
7678
return {

playwright/network.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None
9393
def request(self) -> Request:
9494
return from_channel(self._initializer["request"])
9595

96-
async def abort(self, error_code: str = "failed") -> None:
97-
await self._channel.send("abort", dict(errorCode=error_code))
96+
async def abort(self, errorCode: str = "failed") -> None:
97+
await self._channel.send("abort", dict(errorCode=errorCode))
9898

9999
async def fulfill(
100100
self,

playwright/sync_api.py

+479-617
Large diffs are not rendered by default.

scripts/documentation_provider.py

+141-44
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1-
import json
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
215
import re
3-
from typing import Dict, List
16+
from sys import stderr
17+
from typing import Any, Dict, List, cast
418

519
import requests
620

@@ -26,44 +40,91 @@ def load(self) -> None:
2640
class_name = None
2741
method_name = None
2842
in_a_code_block = False
43+
in_options = False
44+
pending_empty_line = False
45+
2946
for line in api_md.split("\n"):
30-
matches = re.search(r"(class: (\w+)|(Playwright) module)", line)
31-
if matches:
32-
class_name = matches.group(2) or matches.group(3)
33-
method_name = None
34-
if class_name:
35-
if class_name not in self.documentation:
36-
self.documentation[class_name] = {}
37-
matches = re.search(r"#### \w+\.(.+?)(\(|$)", line)
38-
if matches:
39-
method_name = matches.group(1)
40-
# Skip heading
41-
continue
4247
if "```js" in line:
4348
in_a_code_block = True
4449
elif "```" in line:
4550
in_a_code_block = False
46-
elif method_name and not in_a_code_block:
47-
if method_name not in self.documentation[class_name]: # type: ignore
51+
continue
52+
if in_a_code_block:
53+
continue
54+
55+
if line.startswith("### "):
56+
class_name = None
57+
method_name = None
58+
match = re.search(r"### class: (\w+)", line) or re.search(
59+
r"### Playwright module", line
60+
)
61+
if match:
62+
class_name = match.group(1) if match.groups() else "Playwright"
63+
self.documentation[class_name] = {} # type: ignore
64+
continue
65+
if line.startswith("#### "):
66+
match = re.search(r"#### (\w+)\.(.+?)(\(|$)", line)
67+
if match:
68+
if not class_name or match.group(1).lower() != class_name.lower():
69+
print("Error: " + line + " in " + cast(str, class_name))
70+
method_name = match.group(2)
71+
pending_empty_line = False
4872
self.documentation[class_name][method_name] = [] # type: ignore
49-
self.documentation[class_name][method_name].append(line) # type: ignore
50-
51-
def _transform_doc_entry(self, entries: List[str]) -> List[str]:
52-
trimmed = "\n".join(entries).strip().replace("\\", "\\\\")
53-
trimmed = re.sub(r"<\[Array\]<\[(.*?)\]>>", r"<List[\1]>", trimmed)
54-
trimmed = trimmed.replace("Object", "Dict")
55-
trimmed = trimmed.replace("Array", "List")
56-
trimmed = trimmed.replace("boolean", "bool")
57-
trimmed = trimmed.replace("string", "str")
58-
trimmed = trimmed.replace("number", "int")
59-
trimmed = trimmed.replace("Buffer", "bytes")
60-
trimmed = re.sub(r"<\?\[(.*?)\]>", r"<Optional[\1]>", trimmed)
61-
trimmed = re.sub(r"<\[Promise\]<(.*)>>", r"<\1>", trimmed)
62-
trimmed = re.sub(r"<\[(\w+?)\]>", r"<\1>", trimmed)
63-
64-
return trimmed.replace("\n\n\n", "\n\n").split("\n")
65-
66-
def print_entry(self, class_name: str, method_name: str) -> None:
73+
continue
74+
75+
if not method_name: # type: ignore
76+
continue
77+
78+
if (
79+
line.startswith("- `options` <[Object]>")
80+
or line.startswith("- `options` <[string]|[Object]>")
81+
or line.startswith("- `overrides` <")
82+
or line.startswith("- `response` <")
83+
):
84+
in_options = True
85+
continue
86+
if not line.startswith(" "):
87+
in_options = False
88+
if in_options:
89+
line = line[2:]
90+
# if not line.strip():
91+
# continue
92+
if "Shortcut for" in line:
93+
continue
94+
if not line.strip():
95+
pending_empty_line = bool(self.documentation[class_name][method_name]) # type: ignore
96+
continue
97+
else:
98+
if pending_empty_line:
99+
pending_empty_line = False
100+
self.documentation[class_name][method_name].append("") # type: ignore
101+
self.documentation[class_name][method_name].append(line) # type: ignore
102+
103+
def _transform_doc_entry(self, line: str) -> str:
104+
line = line.replace("\\", "\\\\")
105+
line = re.sub(r"<\[Array\]<\[(.*?)\]>>", r"<List[\1]>", line)
106+
line = line.replace("Object", "Dict")
107+
line = line.replace("Array", "List")
108+
line = line.replace("boolean", "bool")
109+
line = line.replace("string", "str")
110+
line = line.replace("number", "int")
111+
line = line.replace("Buffer", "bytes")
112+
line = re.sub(r"<\?\[(.*?)\]>", r"<Optional[\1]>", line)
113+
line = re.sub(r"<\[Promise\]<(.*)>>", r"<\1>", line)
114+
line = re.sub(r"<\[(\w+?)\]>", r"<\1>", line)
115+
116+
# Following should be fixed in the api.md upstream
117+
line = re.sub(r"- `pageFunction` <[^>]+>", "- `expression` <[str]>", line)
118+
line = re.sub("- `urlOrPredicate`", "- `url`", line)
119+
line = re.sub("- `playwrightBinding`", "- `binding`", line)
120+
line = re.sub("- `playwrightFunction`", "- `binding`", line)
121+
line = re.sub("- `script`", "- `source`", line)
122+
123+
return line
124+
125+
def print_entry(
126+
self, class_name: str, method_name: str, signature: Dict[str, Any] = None
127+
) -> None:
67128
if class_name == "BindingCall" or method_name == "pid":
68129
return
69130
if method_name in self.method_name_rewrites:
@@ -75,19 +136,55 @@ def print_entry(self, class_name: str, method_name: str) -> None:
75136
raw_doc = self.documentation["JSHandle"][method_name]
76137
else:
77138
raw_doc = self.documentation[class_name][method_name]
139+
78140
ident = " " * 4 * 2
79-
doc_entries = self._transform_doc_entry(raw_doc)
141+
142+
if signature:
143+
if "return" in signature:
144+
del signature["return"]
145+
80146
print(f'{ident}"""')
81-
for line in doc_entries:
82-
print(f"{ident}{line}")
147+
148+
# Validate signature
149+
validate_parameters = True
150+
for line in raw_doc:
151+
if not line.strip():
152+
validate_parameters = (
153+
False # Stop validating parameters after a blank line
154+
)
155+
156+
transformed = self._transform_doc_entry(line)
157+
match = re.search(r"^\- `(\w+)`", transformed)
158+
if validate_parameters and signature and match:
159+
name = match.group(1)
160+
if name not in signature:
161+
print(
162+
f"Not implemented parameter {class_name}.{method_name}({name}=)",
163+
file=stderr,
164+
)
165+
continue
166+
else:
167+
del signature[name]
168+
print(f"{ident}{transformed}")
169+
if name == "expression" and "force_expr" in signature:
170+
print(
171+
f"{ident}- `force_expr` <[bool]> Whether to treat given expression as JavaScript evaluate expression, even though it looks like an arrow function"
172+
)
173+
del signature["force_expr"]
174+
else:
175+
print(f"{ident}{transformed}")
176+
83177
print(f'{ident}"""')
84178

179+
if signature:
180+
print(
181+
f"Not documented parameters: {class_name}.{method_name}({signature.keys()})",
182+
file=stderr,
183+
)
184+
85185

86186
if __name__ == "__main__":
87-
print(
88-
json.dumps(
89-
DocumentationProvider().documentation["Page"].get("keyboard"),
90-
sort_keys=True,
91-
indent=4,
92-
)
93-
)
187+
DocumentationProvider().print_entry("Page", "goto")
188+
DocumentationProvider().print_entry("Page", "evaluateHandle")
189+
DocumentationProvider().print_entry("ElementHandle", "click")
190+
DocumentationProvider().print_entry("Page", "screenshot")

scripts/generate_async_api.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ def generate(t: Any) -> None:
8181
print(
8282
f" async def {name}({signature(value, len(name) + 9)}) -> {return_type(value)}:"
8383
)
84-
documentation_provider.print_entry(class_name, name)
84+
documentation_provider.print_entry(
85+
class_name, name, get_type_hints(value, api_globals)
86+
)
8587
[prefix, suffix] = return_value(
8688
get_type_hints(value, api_globals)["return"]
8789
)

scripts/generate_sync_api.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ def generate(t: Any) -> None:
7979
print(
8080
f" def {name}({signature(value, len(name) + 9)}) -> {return_type(value)}:"
8181
)
82-
documentation_provider.print_entry(class_name, name)
82+
documentation_provider.print_entry(
83+
class_name, name, get_type_hints(value, api_globals)
84+
)
8385
[prefix, suffix] = return_value(
8486
get_type_hints(value, api_globals)["return"]
8587
)
+21-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
115
from scripts.documentation_provider import DocumentationProvider
216

317

418
def test_transform_documentation_entry() -> None:
519
provider = DocumentationProvider()
6-
assert provider._transform_doc_entry(["<[Promise]<?[Error]>>"]) == [
7-
"<Optional[Error]>"
8-
]
9-
assert provider._transform_doc_entry(["<[Frame]>"]) == ["<Frame>"]
10-
assert provider._transform_doc_entry(["<[function]|[string]|[Object]>"]) == [
11-
"<[function]|[str]|[Dict]>"
12-
]
13-
assert provider._transform_doc_entry(["<?[Object]>"]) == ["<Optional[Dict]>"]
20+
assert provider._transform_doc_entry("<[Promise]<?[Error]>>") == "<Optional[Error]>"
21+
assert provider._transform_doc_entry("<[Frame]>") == "<Frame>"
22+
assert (
23+
provider._transform_doc_entry("<[function]|[string]|[Object]>")
24+
== "<[function]|[str]|[Dict]>"
25+
)
26+
assert provider._transform_doc_entry("<?[Object]>") == "<Optional[Dict]>"

0 commit comments

Comments
 (0)