Skip to content

Commit 319c2a0

Browse files
committed
Setup CLI and include implicit first command arguments
1 parent c5a7223 commit 319c2a0

File tree

10 files changed

+1104
-981
lines changed

10 files changed

+1104
-981
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ This is not a small feat so the stubs will improve over time.
88
- 🚧 maya.cmds: Incomplete
99
- [x] Stubs for all commands.
1010
- [x] Accurate Arguments signatures for most commands (parsed from `cmds.help("command")`).
11+
- [x] Implicit first argument(s) for most command.
1112
- [ ] Accurate Arguments signatures all commands.
12-
- [ ] Implicit first argument of some commands.
1313
- [ ] Return Types.
1414
- [ ] Docstrings.
15-
- 🚫 OpenMaya 1.0: Missing
16-
- 🚫 OpenMaya 2.0: Missing
15+
- OpenMaya 1.0: Missing
16+
- OpenMaya 2.0: Missing

maya-stubs/cmds/__init__.pyi

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

maya_stubgen/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from ._cli import cli
2+
3+
if __name__ == "__main__":
4+
cli()

maya_stubgen/_cli.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import logging
2+
3+
import click
4+
5+
from maya_stubgen.get_cmds_synopsis import get_cmds_synopsis
6+
7+
from .generate_stubs import generate_stubs
8+
from .utils import initialize_maya, uninitialize_maya
9+
10+
logger = logging.getLogger("maya_stubgen")
11+
12+
13+
@click.group
14+
def cli():
15+
initialize_maya()
16+
17+
18+
@cli.result_callback()
19+
def result(result):
20+
uninitialize_maya()
21+
22+
23+
@cli.command
24+
def test_log():
25+
logger.setLevel(logging.DEBUG)
26+
27+
logger.debug("Debug")
28+
logger.info("Info")
29+
logger.success("Success")
30+
logger.warning("Warning")
31+
logger.error("Error")
32+
logger.critical("Critical")
33+
34+
35+
cli.add_command(generate_stubs)
36+
cli.add_command(get_cmds_synopsis)
Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,18 @@
22
import keyword
33
import logging
44
import re
5-
import sys
65
from dataclasses import dataclass, field
76
from pathlib import Path
87
from textwrap import indent
98
from typing import *
109
from typing import Callable
1110

11+
import click
12+
1213
logger = logging.getLogger(__name__)
13-
logger.setLevel(logging.INFO)
1414

1515
DEBUG = False
1616

17-
if __name__ == "__main__":
18-
import maya.standalone
19-
20-
try:
21-
logger.info("Initializing Maya Standalone.")
22-
maya.standalone.initialize()
23-
except BaseException:
24-
logger.info("Failed to initialize Maya Standalone.")
25-
else:
26-
logger.info("Initialized Maya Standalone Successfully.")
27-
2817

2918
from maya import cmds
3019

@@ -137,43 +126,86 @@ def cmds_functions() -> List[Callable]:
137126
return inspect.getmembers(cmds, callable)
138127

139128

140-
def _args_from_docstring(docstring: str) -> List[Argument]:
141-
if "No Flags" in docstring:
129+
def _args_from_help(synopsis: str) -> List[Argument]:
130+
if "No Flags" in synopsis:
142131
arguments = []
143-
elif "Quick help is not available" in docstring:
132+
elif "Quick help is not available" in synopsis:
144133
arguments = ["*args", "**kwargs"]
145134
else:
146135
arguments = []
147-
types_regex = (
148-
r"(\s+((?P<types>[\w\|]+( \w+)?)) ?(?P<multi_use>\(multi-use\))?)?$"
136+
137+
# https://regex101.com/r/9595nC/1
138+
header_regex = r"Synopsis: (?P<name>\w+)( \[flags\] ?(?P<implicit_args>.*))?"
139+
140+
# https://regex101.com/r/bBZoCh/3
141+
flag_regex = (
142+
r"-(?P<short_name>\w+)\s+"
143+
r"-(?P<long_name>\w+)"
144+
r"(?P<types>[\w\|\s\[\]]+)?\s?"
145+
r"(?P<multi_use>\(multi-use\))?\s?"
146+
r"(\(Query Arg (?P<query_arg_mandatory>Mandatory|Optional)\))?"
149147
)
150-
flag_regex = r"^-(?P<short_name>\w+)\s+-(?P<long_name>\S+)" + types_regex
151-
for line in docstring.splitlines():
148+
149+
for line in synopsis.splitlines():
152150
line = line.strip()
153-
match = re.match(flag_regex, line)
154-
if match:
155-
long_name = match["long_name"]
156-
short_name = match["short_name"]
151+
152+
match_header = re.match(header_regex, line)
153+
if match_header:
154+
implicit_args = match_header["implicit_args"]
155+
156+
if not implicit_args:
157+
continue
158+
159+
implicit_args = implicit_args.strip()
160+
161+
if "..." in implicit_args:
162+
# the type is a list. Eg [String...]
163+
implicit_args = implicit_args[1:-1].replace("...", "")
164+
list_type = mel_to_python_type(implicit_args)
165+
arg_type = f"List[{list_type}]"
166+
arg_name = "*args"
167+
elif implicit_args.count(" ") > 0:
168+
# the type is a tuple
169+
implicit_args = implicit_args.replace("[", "").replace("]", "")
170+
tuple_types = map(mel_to_python_type, implicit_args.split())
171+
arg_type = f"Tuple[{', '.join(tuple_types)}]"
172+
arg_name = "*args"
173+
else:
174+
# the type is a basic type
175+
arg_type = mel_to_python_type(implicit_args)
176+
arg_name = "arg0"
177+
178+
argument = Argument(arg_name, None, arg_type)
179+
arguments.append(argument)
180+
181+
continue
182+
183+
match_flag = re.match(flag_regex, line)
184+
if match_flag:
185+
long_name = match_flag["long_name"]
186+
short_name = match_flag["short_name"]
157187

158188
# types can be either
159189
# - One type. eg: Float
160190
# - Multiple types. eg: Float String Int
191+
# - Union of types?. eg: [Float on|off] # TODO: Unsupported
161192
# - None (when no type is specified).
162-
types = str(match["types"]).split()
193+
types = str(match_flag["types"]).split()
163194
types = [mel_to_python_type(t) for t in types]
164195

165196
if len(types) == 1:
166-
type_ = types[0]
197+
arg_type = types[0]
167198
else:
168-
type_ = f"Tuple[{', '.join(types)}]"
199+
arg_type = f"Tuple[{', '.join(types)}]"
169200

170-
multi_use = match["multi_use"]
201+
multi_use = match_flag["multi_use"]
171202
if multi_use:
172-
type_ = f"List[{type_}]"
203+
arg_type = f"List[{arg_type}]"
173204

174-
argument = Argument(long_name, short_name, type_)
205+
argument = Argument(long_name, short_name, arg_type)
175206
if argument not in arguments:
176207
arguments.append(argument)
208+
continue
177209

178210
return arguments
179211

@@ -200,6 +232,8 @@ def mel_to_python_type(type: str) -> str:
200232
"UnsignedInt": "int",
201233
"Time": "int",
202234
# bool
235+
"": "bool",
236+
None: "bool",
203237
"None": "bool",
204238
"on|off": "bool",
205239
}
@@ -217,52 +251,40 @@ def generate_stubs_content() -> str:
217251
for func, _ in cmds_functions():
218252
logger.debug("Generating signature for %s", func)
219253
try:
220-
docstring = cmds.help(func).strip("\n")
254+
help = cmds.help(func).strip("\n")
221255
except RuntimeError:
222-
docstring = ""
256+
help = ""
223257

224-
args = _args_from_docstring(docstring)
258+
args = _args_from_help(help)
225259

226-
function = Function(func, arguments=args, docstring=docstring)
260+
function = Function(func, arguments=args, docstring=help)
227261
lines.append(function.stub)
228262

229263
return "\n".join(lines)
230264

231265

232266
def write_stubs(stubs: str) -> None:
233267
stubs_path = Path(__file__).parent.parent / "maya-stubs" / "cmds" / "__init__.pyi"
268+
234269
logger.debug("Writing stubs in %s", stubs_path)
270+
235271
stubs_path.parent.mkdir(parents=True, exist_ok=True)
236272

237273
with stubs_path.open("w") as f:
238274
f.write(stubs)
239275

276+
return stubs_path
240277

278+
279+
@click.command()
241280
def generate_stubs() -> None:
242281
logger.info("Generating Stubs")
243282

244-
stubs = generate_stubs_content()
245-
write_stubs(stubs)
246-
247-
logger.info("Stubs Generated Successfully")
248-
249-
250-
def main():
251-
global DEBUG
252-
DEBUG = "--debug" in sys.argv
253-
254-
generate_stubs()
255-
256-
257-
if __name__ == "__main__":
258-
main()
259-
260-
import maya.standalone
261-
262283
try:
263-
logger.info("Uninitializing Maya Standalone.")
264-
maya.standalone.uninitialize()
265-
except BaseException:
266-
logger.info("Failed to uninitialize Maya Standalone.")
284+
stubs = generate_stubs_content()
285+
stubs_path = write_stubs(stubs)
286+
except BaseException as e:
287+
logger.error("Failed to generate stubs")
288+
raise e
267289
else:
268-
logger.info("Uninitialized Maya Standalone Successfully.")
290+
logger.success("Stubs generated in %s", stubs_path)

maya_stubgen/get_cmds_synopsis.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import inspect
2+
import logging
3+
from enum import Enum
4+
from pathlib import Path
5+
from typing import *
6+
7+
import click
8+
from maya import cmds
9+
10+
logger = logging.Logger(__name__)
11+
12+
13+
class SynopsisType(Enum):
14+
all = "all"
15+
synopsis = "synopsis"
16+
flags = "flags"
17+
18+
def __str__(self) -> str:
19+
return self.value
20+
21+
22+
def cmds_functions() -> List[Callable]:
23+
return inspect.getmembers(cmds, callable)
24+
25+
26+
def synopsis_for_type(type: SynopsisType):
27+
logger.info("Generating Synopsis for type : %s", type)
28+
all_lines = []
29+
30+
include_synopsis = type is type is SynopsisType.all or type is SynopsisType.synopsis
31+
include_flags = type is type is SynopsisType.all or type is SynopsisType.flags
32+
33+
for func, _ in cmds_functions():
34+
try:
35+
doc = cmds.help(func)
36+
except RuntimeError:
37+
pass
38+
else:
39+
lines = []
40+
41+
synopsis = f"Synopsis: {func}"
42+
43+
for line in doc.splitlines():
44+
line = line.strip()
45+
46+
if "Synopsis" in line:
47+
synopsis = line
48+
49+
if line.startswith("-"):
50+
if include_flags:
51+
lines.append(line)
52+
53+
if type is SynopsisType.all:
54+
synopsis = f"\n{synopsis}"
55+
56+
if include_synopsis:
57+
lines.insert(0, synopsis)
58+
59+
all_lines.extend(lines)
60+
61+
return "\n".join(all_lines).strip()
62+
63+
64+
@click.command
65+
@click.option(
66+
"--type",
67+
default=SynopsisType.all.value,
68+
type=click.Choice(SynopsisType.__members__),
69+
)
70+
def get_cmds_synopsis(type):
71+
72+
type = SynopsisType(type)
73+
cmds_synopsis = synopsis_for_type(type)
74+
75+
out_file = Path() / "out" / "synopsis" / f"{type}.txt"
76+
out_file.parent.mkdir(parents=True, exist_ok=True)
77+
78+
with open(out_file, "w") as f:
79+
f.write(cmds_synopsis)
80+
81+
logger.success("Generated synopsis: %s", out_file)

maya_stubgen/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import logging
2+
from contextlib import contextmanager
3+
from typing import *
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
def initialize_maya():
9+
logger.info("Initializing Maya Standalone")
10+
try:
11+
import maya.standalone
12+
13+
logging.getLogger().handlers
14+
15+
maya.standalone.initialize()
16+
except BaseException:
17+
logger.error("Failed to initialize Maya Standalone")
18+
else:
19+
# remove maya's handler
20+
logging.getLogger().handlers.pop()
21+
logger.success("Maya Standalone Initialized")
22+
23+
24+
def uninitialize_maya():
25+
logger.info("Uninitializing Maya Standalone")
26+
try:
27+
import maya.standalone
28+
29+
maya.standalone.uninitialize()
30+
except BaseException:
31+
logger.error("Failed to uninitialize Maya Standalone")
32+
else:
33+
logger.success("Maya Standalone Uninitialized")
34+
35+
36+
@contextmanager
37+
def maya_standalone():
38+
initialize_maya()
39+
yield
40+
uninitialize_maya()

0 commit comments

Comments
 (0)