Skip to content

Commit 0bb0e34

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

File tree

10 files changed

+1137
-1300
lines changed

10 files changed

+1137
-1300
lines changed

playwright/async_api.py

Lines changed: 479 additions & 619 deletions
Large diffs are not rendered by default.

playwright/browser_context.py

Lines changed: 4 additions & 5 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 4 additions & 2 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 479 additions & 617 deletions
Large diffs are not rendered by default.

scripts/documentation_provider.py

Lines changed: 141 additions & 44 deletions
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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 3 additions & 1 deletion
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
)
Lines changed: 21 additions & 8 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy