omlish 0.0.0.dev66__py3-none-any.whl → 0.0.0.dev68__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omlish/.manifests.json +4 -4
- omlish/__about__.py +2 -2
- omlish/argparse.py +9 -1
- omlish/diag/__init__.py +28 -0
- omlish/diag/_pycharm/__init__.py +0 -0
- omlish/diag/_pycharm/runhack.py +1422 -0
- omlish/diag/pycharm/__init__.py +6 -0
- omlish/diag/pycharm/cli.py +46 -0
- omlish/diag/{pycharm.py → pycharm/pycharm.py} +4 -15
- {omlish-0.0.0.dev66.dist-info → omlish-0.0.0.dev68.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev66.dist-info → omlish-0.0.0.dev68.dist-info}/RECORD +15 -11
- {omlish-0.0.0.dev66.dist-info → omlish-0.0.0.dev68.dist-info}/WHEEL +1 -1
- {omlish-0.0.0.dev66.dist-info → omlish-0.0.0.dev68.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev66.dist-info → omlish-0.0.0.dev68.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev66.dist-info → omlish-0.0.0.dev68.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1422 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
.venv/bin/python $(curl -LsSf https://raw.githubusercontent.com/wrmsr/omlish/master/omlish/diag/_pycharm/runhack.py -o $(mktemp) && echo "$_") install
|
4
|
+
|
5
|
+
==
|
6
|
+
|
7
|
+
See:
|
8
|
+
- https://github.com/JetBrains/intellij-community/blob/6400f70dde6f743e39a257a5a78cc51b644c835e/python/helpers/pycharm/_jb_pytest_runner.py
|
9
|
+
- https://github.com/JetBrains/intellij-community/blob/5a4e584aa59767f2e7cf4bd377adfaaf7503984b/python/helpers/pycharm/_jb_runner_tools.py
|
10
|
+
- https://github.com/JetBrains/intellij-community/blob/5a4e584aa59767f2e7cf4bd377adfaaf7503984b/python/helpers/pydev/_pydevd_bundle/pydevd_command_line_handling.py
|
11
|
+
""" # noqa
|
12
|
+
import os.path
|
13
|
+
import sys
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
ENABLED_ENV_VAR = 'OMLISH_PYCHARM_RUNHACK_ENABLED'
|
20
|
+
DEBUG_ENV_VAR = 'OMLISH_PYCHARM_RUNHACK_DEBUG'
|
21
|
+
|
22
|
+
#
|
23
|
+
|
24
|
+
_DEFAULT_PTH_FILE_NAME = 'omlish-pycharm-runhack.pth'
|
25
|
+
|
26
|
+
_DEFAULT_DEBUG = False
|
27
|
+
_DEFAULT_ENABLED = True
|
28
|
+
|
29
|
+
_DEBUG_PREFIX = 'omlish-pycharm-runhack'
|
30
|
+
|
31
|
+
|
32
|
+
##
|
33
|
+
|
34
|
+
|
35
|
+
class _cached_nullary: # noqa
|
36
|
+
def __init__(self, fn):
|
37
|
+
super().__init__()
|
38
|
+
|
39
|
+
self._fn = fn
|
40
|
+
self._value = self._missing = object()
|
41
|
+
|
42
|
+
def __call__(self, *args, **kwargs): # noqa
|
43
|
+
if self._value is self._missing:
|
44
|
+
self._value = self._fn()
|
45
|
+
|
46
|
+
return self._value
|
47
|
+
|
48
|
+
def __get__(self, instance, owner): # noqa
|
49
|
+
bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
|
50
|
+
return bound
|
51
|
+
|
52
|
+
|
53
|
+
#
|
54
|
+
|
55
|
+
|
56
|
+
def _attr_repr(obj, *atts):
|
57
|
+
return f'{obj.__class__.__name__}({", ".join(f"{a}={getattr(obj, a)!r}" for a in atts)})'
|
58
|
+
|
59
|
+
|
60
|
+
def _attr_dict(obj, *atts):
|
61
|
+
return {a: getattr(obj, a) for a in atts}
|
62
|
+
|
63
|
+
|
64
|
+
#
|
65
|
+
|
66
|
+
|
67
|
+
def _check_not_none(obj):
|
68
|
+
if obj is None:
|
69
|
+
raise RuntimeError
|
70
|
+
return obj
|
71
|
+
|
72
|
+
|
73
|
+
#
|
74
|
+
|
75
|
+
|
76
|
+
_BOOL_ENV_VAR_VALUES = {
|
77
|
+
s: b
|
78
|
+
for b, ss in [
|
79
|
+
(True, ['1', 'true', 't']),
|
80
|
+
(False, ['0', 'false', 'f']),
|
81
|
+
]
|
82
|
+
for s in ss
|
83
|
+
}
|
84
|
+
|
85
|
+
|
86
|
+
def _get_opt_env_bool(n, d): # type: (str | None, bool) -> bool
|
87
|
+
if n is None or n not in os.environ:
|
88
|
+
return d
|
89
|
+
return _BOOL_ENV_VAR_VALUES[os.environ[n]]
|
90
|
+
|
91
|
+
|
92
|
+
def _get_env_path_list(k): # type: (str) -> list[str]
|
93
|
+
v = os.environ.get(k, '')
|
94
|
+
if v:
|
95
|
+
return v.split(os.pathsep)
|
96
|
+
else:
|
97
|
+
return []
|
98
|
+
|
99
|
+
|
100
|
+
##
|
101
|
+
|
102
|
+
|
103
|
+
class AttrsClass:
|
104
|
+
__attrs__ = () # type: tuple[str, ...]
|
105
|
+
|
106
|
+
def __repr__(self) -> str:
|
107
|
+
return _attr_repr(self, *self.__attrs__)
|
108
|
+
|
109
|
+
def attrs_dict(self): # type: () -> dict[str, object]
|
110
|
+
return {a: getattr(self, a) for a in self.__attrs__}
|
111
|
+
|
112
|
+
def replace(self, **kwargs):
|
113
|
+
return self.__class__(**{**self.attrs_dict(), **kwargs})
|
114
|
+
|
115
|
+
|
116
|
+
class AsJson:
|
117
|
+
def as_json(self): # type: () -> dict[str, object]
|
118
|
+
raise TypeError
|
119
|
+
|
120
|
+
|
121
|
+
##
|
122
|
+
|
123
|
+
|
124
|
+
class RunEnv(AttrsClass, AsJson):
|
125
|
+
def __init__(
|
126
|
+
self,
|
127
|
+
*,
|
128
|
+
argv=None, # type: list[str] | None
|
129
|
+
orig_argv=None, # type: list[str] | None
|
130
|
+
|
131
|
+
cwd=None, # type: str | None
|
132
|
+
|
133
|
+
library_roots=None, # type: list[str] | None
|
134
|
+
path=None, # type: list[str] | None
|
135
|
+
python_path=None, # type: list[str] | None
|
136
|
+
ide_project_roots=None, # type: list[str] | None
|
137
|
+
pycharm_hosted=None, # type: bool | None
|
138
|
+
|
139
|
+
sys_path=None, # type: list[str] | None
|
140
|
+
) -> None:
|
141
|
+
super().__init__()
|
142
|
+
|
143
|
+
if argv is None:
|
144
|
+
argv = sys.argv
|
145
|
+
self._argv = list(argv)
|
146
|
+
|
147
|
+
if orig_argv is None:
|
148
|
+
orig_argv = sys.orig_argv
|
149
|
+
self._orig_argv = list(orig_argv)
|
150
|
+
|
151
|
+
if cwd is None:
|
152
|
+
cwd = os.getcwd()
|
153
|
+
self._cwd = cwd
|
154
|
+
|
155
|
+
if library_roots is None:
|
156
|
+
library_roots = _get_env_path_list('LIBRARY_ROOTS')
|
157
|
+
self._library_roots = list(library_roots)
|
158
|
+
|
159
|
+
if path is None:
|
160
|
+
path = _get_env_path_list('PATH')
|
161
|
+
self._path = list(path)
|
162
|
+
|
163
|
+
if python_path is None:
|
164
|
+
python_path = _get_env_path_list('PYTHONPATH')
|
165
|
+
self._python_path = list(python_path)
|
166
|
+
|
167
|
+
if ide_project_roots is None:
|
168
|
+
ide_project_roots = _get_env_path_list('IDE_PROJECT_ROOTS')
|
169
|
+
self._ide_project_roots = list(ide_project_roots)
|
170
|
+
|
171
|
+
if pycharm_hosted is None:
|
172
|
+
pycharm_hosted = 'PYCHARM_HOSTED' in os.environ
|
173
|
+
self._pycharm_hosted = pycharm_hosted
|
174
|
+
|
175
|
+
if sys_path is None:
|
176
|
+
sys_path = list(sys.path)
|
177
|
+
self._sys_path = sys_path
|
178
|
+
|
179
|
+
__attrs__ = (
|
180
|
+
'argv',
|
181
|
+
'orig_argv',
|
182
|
+
|
183
|
+
'cwd',
|
184
|
+
|
185
|
+
'library_roots',
|
186
|
+
'path',
|
187
|
+
'python_path',
|
188
|
+
'ide_project_roots',
|
189
|
+
'pycharm_hosted',
|
190
|
+
|
191
|
+
'sys_path',
|
192
|
+
)
|
193
|
+
|
194
|
+
@property
|
195
|
+
def argv(self): # type: () -> list[str]
|
196
|
+
return self._argv
|
197
|
+
|
198
|
+
@property
|
199
|
+
def orig_argv(self): # type: () -> list[str]
|
200
|
+
return self._orig_argv
|
201
|
+
|
202
|
+
@property
|
203
|
+
def cwd(self): # type: () -> str
|
204
|
+
return self._cwd
|
205
|
+
|
206
|
+
@property
|
207
|
+
def library_roots(self): # type: () -> list[str]
|
208
|
+
return self._library_roots
|
209
|
+
|
210
|
+
@property
|
211
|
+
def path(self): # type: () -> list[str]
|
212
|
+
return self._path
|
213
|
+
|
214
|
+
@property
|
215
|
+
def python_path(self): # type: () -> list[str]
|
216
|
+
return self._python_path
|
217
|
+
|
218
|
+
@property
|
219
|
+
def ide_project_roots(self): # type: () -> list[str]
|
220
|
+
return self._ide_project_roots
|
221
|
+
|
222
|
+
@property
|
223
|
+
def pycharm_hosted(self): # type: () -> bool
|
224
|
+
return self._pycharm_hosted
|
225
|
+
|
226
|
+
@property
|
227
|
+
def sys_path(self): # type: () -> list[str]
|
228
|
+
return self._sys_path
|
229
|
+
|
230
|
+
def as_json(self): # type: () -> dict[str, object]
|
231
|
+
return self.attrs_dict()
|
232
|
+
|
233
|
+
|
234
|
+
##
|
235
|
+
|
236
|
+
|
237
|
+
class Param:
|
238
|
+
def __init__(
|
239
|
+
self,
|
240
|
+
name: str,
|
241
|
+
cls, # type: type[Arg]
|
242
|
+
) -> None:
|
243
|
+
super().__init__()
|
244
|
+
|
245
|
+
if not issubclass(cls, Arg):
|
246
|
+
raise TypeError(cls)
|
247
|
+
|
248
|
+
self._name = name
|
249
|
+
self._cls = cls
|
250
|
+
|
251
|
+
@property
|
252
|
+
def name(self) -> str:
|
253
|
+
return self._name
|
254
|
+
|
255
|
+
@property
|
256
|
+
def cls(self): # type: () -> type[Arg]
|
257
|
+
return self._cls
|
258
|
+
|
259
|
+
def __repr__(self) -> str:
|
260
|
+
return _attr_repr(self, 'name', 'cls')
|
261
|
+
|
262
|
+
|
263
|
+
class Params:
|
264
|
+
def __init__(
|
265
|
+
self,
|
266
|
+
params, # type: list[Param]
|
267
|
+
) -> None:
|
268
|
+
super().__init__()
|
269
|
+
|
270
|
+
self._params = params
|
271
|
+
self._params_by_name = {} # type: dict[str, Param]
|
272
|
+
|
273
|
+
for p in params:
|
274
|
+
if p.name in self._params_by_name:
|
275
|
+
raise KeyError(p.name)
|
276
|
+
self._params_by_name[p.name] = p
|
277
|
+
|
278
|
+
@property
|
279
|
+
def params(self): # type: () -> list[Param]
|
280
|
+
return self._params
|
281
|
+
|
282
|
+
@property
|
283
|
+
def params_by_name(self): # type: () -> dict[str, Param]
|
284
|
+
return self._params_by_name
|
285
|
+
|
286
|
+
def __repr__(self) -> str:
|
287
|
+
return _attr_repr(self, 'params')
|
288
|
+
|
289
|
+
|
290
|
+
#
|
291
|
+
|
292
|
+
|
293
|
+
class Arg(AsJson):
|
294
|
+
def __init__(self, param: Param) -> None:
|
295
|
+
super().__init__()
|
296
|
+
|
297
|
+
if not isinstance(param, Param):
|
298
|
+
raise TypeError(param)
|
299
|
+
|
300
|
+
self._param = param
|
301
|
+
|
302
|
+
@property
|
303
|
+
def param(self) -> Param:
|
304
|
+
return self._param
|
305
|
+
|
306
|
+
|
307
|
+
class BoolArg(Arg):
|
308
|
+
def __repr__(self) -> str:
|
309
|
+
return _attr_repr(self, 'param')
|
310
|
+
|
311
|
+
def as_json(self): # type: () -> dict[str, object]
|
312
|
+
return {self._param.name: True}
|
313
|
+
|
314
|
+
|
315
|
+
class StrArg(Arg):
|
316
|
+
def __init__(self, param: Param, value: str) -> None:
|
317
|
+
super().__init__(param)
|
318
|
+
|
319
|
+
self._value = value
|
320
|
+
|
321
|
+
@property
|
322
|
+
def value(self) -> str:
|
323
|
+
return self._value
|
324
|
+
|
325
|
+
def __repr__(self) -> str:
|
326
|
+
return _attr_repr(self, 'param', 'value')
|
327
|
+
|
328
|
+
def as_json(self): # type: () -> dict[str, object]
|
329
|
+
return {self._param.name: self._value}
|
330
|
+
|
331
|
+
|
332
|
+
class OptStrArg(Arg):
|
333
|
+
def __init__(
|
334
|
+
self,
|
335
|
+
param: Param,
|
336
|
+
value, # type: str | None
|
337
|
+
) -> None:
|
338
|
+
super().__init__(param)
|
339
|
+
|
340
|
+
self._value = value
|
341
|
+
|
342
|
+
@property
|
343
|
+
def value(self): # type: () -> str | None
|
344
|
+
return self._value
|
345
|
+
|
346
|
+
def __repr__(self) -> str:
|
347
|
+
return _attr_repr(self, 'param', 'value')
|
348
|
+
|
349
|
+
def as_json(self): # type: () -> dict[str, object]
|
350
|
+
return {self._param.name: self._value}
|
351
|
+
|
352
|
+
|
353
|
+
class FinalArg(Arg):
|
354
|
+
def __init__(
|
355
|
+
self,
|
356
|
+
param: Param,
|
357
|
+
values, # type: list[str]
|
358
|
+
) -> None:
|
359
|
+
super().__init__(param)
|
360
|
+
|
361
|
+
self._values = values
|
362
|
+
|
363
|
+
@property
|
364
|
+
def values(self): # type: () -> list[str]
|
365
|
+
return self._values
|
366
|
+
|
367
|
+
def __repr__(self) -> str:
|
368
|
+
return _attr_repr(self, 'param', 'values')
|
369
|
+
|
370
|
+
def as_json(self): # type: () -> dict[str, object]
|
371
|
+
return {self._param.name: self._values}
|
372
|
+
|
373
|
+
|
374
|
+
class Args(AsJson):
|
375
|
+
def __init__(
|
376
|
+
self,
|
377
|
+
params: Params,
|
378
|
+
args, # type: list[Arg]
|
379
|
+
) -> None:
|
380
|
+
super().__init__()
|
381
|
+
|
382
|
+
self._params = params
|
383
|
+
self._args = args
|
384
|
+
|
385
|
+
self._arg_lists_by_name = {} # type: dict[str, list[Arg]]
|
386
|
+
for a in args:
|
387
|
+
self._arg_lists_by_name.setdefault(a.param.name, []).append(a)
|
388
|
+
|
389
|
+
@property
|
390
|
+
def params(self): # type: () -> Params
|
391
|
+
return self._params
|
392
|
+
|
393
|
+
@property
|
394
|
+
def args(self): # type: () -> list[Arg]
|
395
|
+
return self._args
|
396
|
+
|
397
|
+
@property
|
398
|
+
def arg_lists_by_name(self): # type: () -> dict[str, list[Arg]]
|
399
|
+
return self._arg_lists_by_name
|
400
|
+
|
401
|
+
def without(self, *names: str) -> 'Args':
|
402
|
+
return Args(
|
403
|
+
self._params,
|
404
|
+
[a for a in self._args if a.param.name not in names],
|
405
|
+
)
|
406
|
+
|
407
|
+
def __repr__(self) -> str:
|
408
|
+
return _attr_repr(self, 'args')
|
409
|
+
|
410
|
+
def as_json(self): # type: () -> dict[str, object]
|
411
|
+
return {k: v for a in self._args for k, v in a.as_json().items()}
|
412
|
+
|
413
|
+
|
414
|
+
#
|
415
|
+
|
416
|
+
|
417
|
+
class ArgParseError(Exception):
|
418
|
+
pass
|
419
|
+
|
420
|
+
|
421
|
+
def parse_args(
|
422
|
+
params: Params,
|
423
|
+
argv, # type: list[str]
|
424
|
+
) -> Args:
|
425
|
+
l = [] # type: list[Arg]
|
426
|
+
|
427
|
+
it = iter(argv)
|
428
|
+
for s in it:
|
429
|
+
if len(s) > 1 and s.startswith('--'):
|
430
|
+
s = s[2:]
|
431
|
+
else:
|
432
|
+
raise ArgParseError(s, argv)
|
433
|
+
|
434
|
+
if '=' in s:
|
435
|
+
k, _, v = s.partition('=')
|
436
|
+
else:
|
437
|
+
k, v = s, None
|
438
|
+
|
439
|
+
p = params.params_by_name[k]
|
440
|
+
|
441
|
+
if p.cls is BoolArg:
|
442
|
+
if v is not None:
|
443
|
+
raise ArgParseError(s, argv)
|
444
|
+
l.append(BoolArg(p))
|
445
|
+
|
446
|
+
elif p.cls is StrArg:
|
447
|
+
if v is None:
|
448
|
+
try:
|
449
|
+
v = next(it)
|
450
|
+
except StopIteration:
|
451
|
+
raise ArgParseError(s, argv) # noqa
|
452
|
+
l.append(StrArg(p, v))
|
453
|
+
|
454
|
+
elif p.cls is OptStrArg:
|
455
|
+
l.append(OptStrArg(p, v))
|
456
|
+
|
457
|
+
elif p.cls is FinalArg:
|
458
|
+
vs = [] # type: list[str]
|
459
|
+
if v is not None:
|
460
|
+
vs.append(v)
|
461
|
+
vs.extend(it)
|
462
|
+
l.append(FinalArg(p, vs))
|
463
|
+
|
464
|
+
else:
|
465
|
+
raise TypeError(p.cls)
|
466
|
+
|
467
|
+
return Args(params, l)
|
468
|
+
|
469
|
+
|
470
|
+
#
|
471
|
+
|
472
|
+
|
473
|
+
def render_arg(arg): # type: (Arg) -> list[str]
|
474
|
+
if isinstance(arg, BoolArg):
|
475
|
+
return [f'--{arg.param.name}']
|
476
|
+
|
477
|
+
elif isinstance(arg, StrArg):
|
478
|
+
return [f'--{arg.param.name}', arg.value]
|
479
|
+
|
480
|
+
elif isinstance(arg, OptStrArg):
|
481
|
+
if arg.value is None:
|
482
|
+
return [f'--{arg.param.name}']
|
483
|
+
else:
|
484
|
+
return [f'--{arg.param.name}={arg.value}']
|
485
|
+
|
486
|
+
elif isinstance(arg, FinalArg):
|
487
|
+
return [f'--{arg.param.name}', *arg.values] # noqa
|
488
|
+
|
489
|
+
else:
|
490
|
+
raise TypeError(arg)
|
491
|
+
|
492
|
+
|
493
|
+
def render_args(args): # type: (list[Arg]) -> list[str]
|
494
|
+
return [ra for a in args for ra in render_arg(a)]
|
495
|
+
|
496
|
+
|
497
|
+
##
|
498
|
+
|
499
|
+
|
500
|
+
class Target(AttrsClass, AsJson):
|
501
|
+
pass
|
502
|
+
|
503
|
+
|
504
|
+
#
|
505
|
+
|
506
|
+
|
507
|
+
class UserTarget(Target):
|
508
|
+
def __init__(
|
509
|
+
self,
|
510
|
+
argv, # type: list[str]
|
511
|
+
) -> None:
|
512
|
+
super().__init__()
|
513
|
+
|
514
|
+
self._argv = argv
|
515
|
+
|
516
|
+
@property
|
517
|
+
def argv(self): # type: () -> list[str]
|
518
|
+
return self._argv
|
519
|
+
|
520
|
+
|
521
|
+
class FileTarget(UserTarget):
|
522
|
+
def __init__(
|
523
|
+
self,
|
524
|
+
file: str,
|
525
|
+
argv, # type: list[str]
|
526
|
+
) -> None:
|
527
|
+
super().__init__(argv)
|
528
|
+
|
529
|
+
self._file = file
|
530
|
+
|
531
|
+
@property
|
532
|
+
def file(self) -> str:
|
533
|
+
return self._file
|
534
|
+
|
535
|
+
__attrs__ = ('file', 'argv')
|
536
|
+
|
537
|
+
def as_json(self): # type: () -> dict[str, object]
|
538
|
+
return self.attrs_dict()
|
539
|
+
|
540
|
+
|
541
|
+
class ModuleTarget(UserTarget):
|
542
|
+
def __init__(
|
543
|
+
self,
|
544
|
+
module: str,
|
545
|
+
argv, # type: list[str]
|
546
|
+
) -> None:
|
547
|
+
super().__init__(argv)
|
548
|
+
|
549
|
+
self._module = module
|
550
|
+
|
551
|
+
@property
|
552
|
+
def module(self) -> str:
|
553
|
+
return self._module
|
554
|
+
|
555
|
+
__attrs__ = ('module', 'argv')
|
556
|
+
|
557
|
+
def as_json(self): # type: () -> dict[str, object]
|
558
|
+
return self.attrs_dict()
|
559
|
+
|
560
|
+
|
561
|
+
#
|
562
|
+
|
563
|
+
|
564
|
+
class PycharmTarget(Target):
|
565
|
+
def __init__(self, file: str, args: Args) -> None:
|
566
|
+
super().__init__()
|
567
|
+
|
568
|
+
if not isinstance(file, str):
|
569
|
+
raise TypeError(file)
|
570
|
+
self._file = file
|
571
|
+
|
572
|
+
if not isinstance(args, Args):
|
573
|
+
raise TypeError(args)
|
574
|
+
self._args = args
|
575
|
+
|
576
|
+
@property
|
577
|
+
def file(self) -> str:
|
578
|
+
return self._file
|
579
|
+
|
580
|
+
@property
|
581
|
+
def args(self) -> Args:
|
582
|
+
return self._args
|
583
|
+
|
584
|
+
|
585
|
+
class DebuggerTarget(PycharmTarget):
|
586
|
+
def __init__(self, file: str, args: Args, target: Target) -> None:
|
587
|
+
super().__init__(file, args)
|
588
|
+
|
589
|
+
if isinstance(target, DebuggerTarget):
|
590
|
+
raise TypeError(target)
|
591
|
+
self._target = target
|
592
|
+
|
593
|
+
@property
|
594
|
+
def target(self) -> Target:
|
595
|
+
return self._target
|
596
|
+
|
597
|
+
__attrs__ = ('file', 'args', 'target')
|
598
|
+
|
599
|
+
def as_json(self): # type: () -> dict[str, object]
|
600
|
+
return {
|
601
|
+
'debugger': self._file,
|
602
|
+
'args': self._args.as_json(),
|
603
|
+
'target': self._target.as_json(),
|
604
|
+
}
|
605
|
+
|
606
|
+
|
607
|
+
class Test(AsJson):
|
608
|
+
def __init__(self, s: str) -> None:
|
609
|
+
super().__init__()
|
610
|
+
|
611
|
+
if not isinstance(s, str):
|
612
|
+
raise TypeError(s)
|
613
|
+
|
614
|
+
self._s = s
|
615
|
+
|
616
|
+
@property
|
617
|
+
def s(self) -> str:
|
618
|
+
return self._s
|
619
|
+
|
620
|
+
def __repr__(self) -> str:
|
621
|
+
return _attr_repr(self, 's')
|
622
|
+
|
623
|
+
|
624
|
+
class PathTest(Test):
|
625
|
+
def as_json(self): # type: () -> dict[str, object]
|
626
|
+
return {'path': self._s}
|
627
|
+
|
628
|
+
|
629
|
+
class TargetTest(Test):
|
630
|
+
def as_json(self): # type: () -> dict[str, object]
|
631
|
+
return {'target': self._s}
|
632
|
+
|
633
|
+
|
634
|
+
class TestRunnerTarget(PycharmTarget):
|
635
|
+
def __init__(
|
636
|
+
self,
|
637
|
+
file: str,
|
638
|
+
args: Args,
|
639
|
+
tests, # type: list[Test]
|
640
|
+
) -> None:
|
641
|
+
super().__init__(file, args)
|
642
|
+
|
643
|
+
self._tests = tests
|
644
|
+
|
645
|
+
@property
|
646
|
+
def tests(self): # type: () -> list[Test]
|
647
|
+
return self._tests
|
648
|
+
|
649
|
+
__attrs__ = ('file', 'args', 'tests')
|
650
|
+
|
651
|
+
def as_json(self): # type: () -> dict[str, object]
|
652
|
+
return {
|
653
|
+
'test_runner': self._file,
|
654
|
+
'args': self._args.as_json(),
|
655
|
+
'tests': [t.as_json() for t in self._tests],
|
656
|
+
}
|
657
|
+
|
658
|
+
|
659
|
+
#
|
660
|
+
|
661
|
+
|
662
|
+
def is_pycharm_dir(s: str) -> bool:
|
663
|
+
s = os.path.abspath(s)
|
664
|
+
if not os.path.isdir(s):
|
665
|
+
return False
|
666
|
+
|
667
|
+
ps = s.split(os.sep)
|
668
|
+
|
669
|
+
plat = getattr(sys, 'platform')
|
670
|
+
if plat == 'darwin':
|
671
|
+
# /Applications/PyCharm.app/Contents/bin/pycharm.vmoptions
|
672
|
+
return ps[-1] == 'Contents' and os.path.isfile(os.path.join(s, 'bin', 'pycharm.vmoptions'))
|
673
|
+
|
674
|
+
if plat == 'linux':
|
675
|
+
# /snap/pycharm-professional/current/bin/pycharm64.vmoptions
|
676
|
+
return os.path.isfile(os.path.join(s, 'bin', 'pycharm64.vmoptions'))
|
677
|
+
|
678
|
+
return False
|
679
|
+
|
680
|
+
|
681
|
+
def is_pycharm_file(given: str, expected: str) -> bool:
|
682
|
+
dgs = os.path.abspath(given).split(os.sep)
|
683
|
+
des = expected.split(os.sep)
|
684
|
+
return (
|
685
|
+
len(des) < len(dgs) and
|
686
|
+
dgs[-len(des):] == des and
|
687
|
+
is_pycharm_dir(os.sep.join(dgs[:-len(des)]))
|
688
|
+
)
|
689
|
+
|
690
|
+
|
691
|
+
class PycharmEntrypoint:
|
692
|
+
def __init__(self, file: str, params: Params) -> None:
|
693
|
+
super().__init__()
|
694
|
+
|
695
|
+
self._file = file
|
696
|
+
self._params = params
|
697
|
+
|
698
|
+
@property
|
699
|
+
def file(self) -> str:
|
700
|
+
return self._file
|
701
|
+
|
702
|
+
@property
|
703
|
+
def params(self) -> Params:
|
704
|
+
return self._params
|
705
|
+
|
706
|
+
def __repr__(self) -> str:
|
707
|
+
return _attr_repr(self, 'file', 'params')
|
708
|
+
|
709
|
+
|
710
|
+
DEBUGGER_ENTRYPOINT = PycharmEntrypoint(
|
711
|
+
'plugins/python-ce/helpers/pydev/pydevd.py',
|
712
|
+
Params([
|
713
|
+
Param('port', StrArg),
|
714
|
+
Param('vm_type', StrArg),
|
715
|
+
Param('client', StrArg),
|
716
|
+
|
717
|
+
Param('qt-support', OptStrArg),
|
718
|
+
|
719
|
+
Param('file', FinalArg),
|
720
|
+
|
721
|
+
Param('server', BoolArg),
|
722
|
+
Param('DEBUG_RECORD_SOCKET_READS', BoolArg),
|
723
|
+
Param('multiproc', BoolArg),
|
724
|
+
Param('multiprocess', BoolArg),
|
725
|
+
Param('save-signatures', BoolArg),
|
726
|
+
Param('save-threading', BoolArg),
|
727
|
+
Param('save-asyncio', BoolArg),
|
728
|
+
Param('print-in-debugger-startup', BoolArg),
|
729
|
+
Param('cmd-line', BoolArg),
|
730
|
+
Param('module', BoolArg),
|
731
|
+
Param('help', BoolArg),
|
732
|
+
Param('DEBUG', BoolArg),
|
733
|
+
]),
|
734
|
+
)
|
735
|
+
|
736
|
+
|
737
|
+
TEST_RUNNER_ENTRYPOINT = PycharmEntrypoint(
|
738
|
+
'plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py',
|
739
|
+
Params([
|
740
|
+
Param('path', StrArg),
|
741
|
+
Param('offset', StrArg),
|
742
|
+
Param('target', StrArg),
|
743
|
+
|
744
|
+
Param('', FinalArg),
|
745
|
+
]),
|
746
|
+
)
|
747
|
+
|
748
|
+
|
749
|
+
def try_parse_entrypoint_args(ep, argv): # type: (PycharmEntrypoint, list[str]) -> Args | None
|
750
|
+
if not argv:
|
751
|
+
return None
|
752
|
+
|
753
|
+
if not is_pycharm_file(argv[0], ep.file):
|
754
|
+
return None
|
755
|
+
|
756
|
+
return parse_args(ep.params, argv[1:])
|
757
|
+
|
758
|
+
|
759
|
+
def _make_module_target(
|
760
|
+
argv, # type: list[str]
|
761
|
+
) -> ModuleTarget:
|
762
|
+
if argv[0] == '-m':
|
763
|
+
return ModuleTarget(argv[1], argv[2:])
|
764
|
+
elif argv[0].startswith('-m'):
|
765
|
+
return ModuleTarget(argv[0][2:], argv[1:])
|
766
|
+
else:
|
767
|
+
raise ArgParseError(argv)
|
768
|
+
|
769
|
+
|
770
|
+
def parse_args_target(
|
771
|
+
argv, # type: list[str]
|
772
|
+
) -> Target:
|
773
|
+
if not argv:
|
774
|
+
raise Exception
|
775
|
+
|
776
|
+
elif (pa := try_parse_entrypoint_args(DEBUGGER_ENTRYPOINT, argv)) is not None:
|
777
|
+
fa = pa.args[-1]
|
778
|
+
if not isinstance(fa, FinalArg) or fa.param.name != 'file':
|
779
|
+
raise TypeError(fa)
|
780
|
+
|
781
|
+
st = parse_args_target(fa.values)
|
782
|
+
|
783
|
+
if isinstance(st, TestRunnerTarget):
|
784
|
+
if 'module' in pa.arg_lists_by_name:
|
785
|
+
raise ArgParseError(argv)
|
786
|
+
|
787
|
+
elif isinstance(st, FileTarget):
|
788
|
+
if 'module' in pa.arg_lists_by_name:
|
789
|
+
st = ModuleTarget(st.file, st.argv)
|
790
|
+
|
791
|
+
else:
|
792
|
+
raise TypeError(st)
|
793
|
+
|
794
|
+
return DebuggerTarget(
|
795
|
+
argv[0],
|
796
|
+
pa.without('file', 'module'),
|
797
|
+
st,
|
798
|
+
)
|
799
|
+
|
800
|
+
elif (pa := try_parse_entrypoint_args(TEST_RUNNER_ENTRYPOINT, argv)) is not None:
|
801
|
+
ts = [] # type: list[Test]
|
802
|
+
for a in pa.args:
|
803
|
+
if isinstance(a, StrArg):
|
804
|
+
if a.param.name == 'path':
|
805
|
+
ts.append(PathTest(a.value))
|
806
|
+
elif a.param.name == 'target':
|
807
|
+
ts.append(TargetTest(a.value))
|
808
|
+
|
809
|
+
return TestRunnerTarget(
|
810
|
+
argv[0],
|
811
|
+
pa.without('path', 'target'),
|
812
|
+
ts,
|
813
|
+
)
|
814
|
+
|
815
|
+
elif argv[0].startswith('-m'):
|
816
|
+
return _make_module_target(argv)
|
817
|
+
|
818
|
+
else:
|
819
|
+
return FileTarget(argv[0], argv[1:])
|
820
|
+
|
821
|
+
|
822
|
+
#
|
823
|
+
|
824
|
+
|
825
|
+
def render_target_args(tgt): # type: (Target) -> list[str]
|
826
|
+
if isinstance(tgt, FileTarget):
|
827
|
+
return [tgt.file, *tgt.argv]
|
828
|
+
|
829
|
+
elif isinstance(tgt, ModuleTarget):
|
830
|
+
return ['-m', *tgt.argv]
|
831
|
+
|
832
|
+
elif isinstance(tgt, DebuggerTarget):
|
833
|
+
l = [
|
834
|
+
tgt.file,
|
835
|
+
*render_args(tgt.args.args),
|
836
|
+
]
|
837
|
+
dt = tgt.target
|
838
|
+
if isinstance(dt, ModuleTarget):
|
839
|
+
l.extend(['--module', '--file', dt.module, *dt.argv])
|
840
|
+
else:
|
841
|
+
l.extend(['--file', *render_target_args(dt)])
|
842
|
+
return l
|
843
|
+
|
844
|
+
elif isinstance(tgt, TestRunnerTarget):
|
845
|
+
l = [
|
846
|
+
tgt.file,
|
847
|
+
]
|
848
|
+
for t in tgt.tests:
|
849
|
+
if isinstance(t, PathTest):
|
850
|
+
l.extend(['--path', t.s])
|
851
|
+
elif isinstance(t, TargetTest):
|
852
|
+
l.extend(['--target', t.s])
|
853
|
+
else:
|
854
|
+
raise TypeError(t)
|
855
|
+
l.extend(render_args(tgt.args.args))
|
856
|
+
return l
|
857
|
+
|
858
|
+
else:
|
859
|
+
raise TypeError(tgt)
|
860
|
+
|
861
|
+
|
862
|
+
##
|
863
|
+
|
864
|
+
|
865
|
+
class Exec(AttrsClass, AsJson):
|
866
|
+
def __init__(
|
867
|
+
self,
|
868
|
+
exe: str,
|
869
|
+
exe_args, # type: list[str]
|
870
|
+
target: Target,
|
871
|
+
) -> None:
|
872
|
+
super().__init__()
|
873
|
+
|
874
|
+
self._exe = exe
|
875
|
+
self._exe_args = exe_args
|
876
|
+
self._target = target
|
877
|
+
|
878
|
+
@property
|
879
|
+
def exe(self) -> str:
|
880
|
+
return self._exe
|
881
|
+
|
882
|
+
@property
|
883
|
+
def exe_args(self): # type: () -> list[str]
|
884
|
+
return self._exe_args
|
885
|
+
|
886
|
+
@property
|
887
|
+
def target(self) -> Target:
|
888
|
+
return self._target
|
889
|
+
|
890
|
+
__attrs__ = ('exe', 'exe_args', 'target')
|
891
|
+
|
892
|
+
def as_json(self): # type: () -> dict[str, object]
|
893
|
+
return {
|
894
|
+
'exe': self._exe,
|
895
|
+
'exe_args': self._exe_args,
|
896
|
+
'target': self._target.as_json(),
|
897
|
+
}
|
898
|
+
|
899
|
+
|
900
|
+
def parse_exec(
|
901
|
+
exe_argv, # type: list[str]
|
902
|
+
) -> Exec:
|
903
|
+
it = iter(exe_argv)
|
904
|
+
exe = next(it)
|
905
|
+
|
906
|
+
exe_args = [] # type: list[str]
|
907
|
+
|
908
|
+
for a in it:
|
909
|
+
if a.startswith('-X'):
|
910
|
+
if a == '-X':
|
911
|
+
exe_args.extend([a, next(it)])
|
912
|
+
else:
|
913
|
+
exe_args.append(a)
|
914
|
+
else:
|
915
|
+
break
|
916
|
+
else:
|
917
|
+
raise Exception(exe_argv)
|
918
|
+
|
919
|
+
argv = [a, *it]
|
920
|
+
|
921
|
+
if argv[0].startswith('-m'):
|
922
|
+
tgt = _make_module_target(argv) # type: Target
|
923
|
+
|
924
|
+
else:
|
925
|
+
tgt = parse_args_target(argv)
|
926
|
+
|
927
|
+
return Exec(
|
928
|
+
exe,
|
929
|
+
exe_args,
|
930
|
+
tgt,
|
931
|
+
)
|
932
|
+
|
933
|
+
|
934
|
+
def render_exec_args(exe): # type: (Exec) -> list[str]
|
935
|
+
l = [
|
936
|
+
exe.exe,
|
937
|
+
*exe.exe_args,
|
938
|
+
]
|
939
|
+
|
940
|
+
et = exe.target
|
941
|
+
|
942
|
+
if isinstance(et, ModuleTarget):
|
943
|
+
l.extend(['-m', et.module, *et.argv])
|
944
|
+
|
945
|
+
else:
|
946
|
+
l.extend(render_target_args(et))
|
947
|
+
|
948
|
+
return l
|
949
|
+
|
950
|
+
|
951
|
+
##
|
952
|
+
|
953
|
+
|
954
|
+
class ExecDecision(AttrsClass, AsJson):
|
955
|
+
def __init__(
|
956
|
+
self,
|
957
|
+
target: Target,
|
958
|
+
*,
|
959
|
+
cwd=None, # type: str | None
|
960
|
+
python_path=None, # type: list[str] | None
|
961
|
+
sys_path=None, # type: list[str] | None
|
962
|
+
os_exec: bool = False,
|
963
|
+
) -> None:
|
964
|
+
super().__init__()
|
965
|
+
|
966
|
+
if not isinstance(target, Target):
|
967
|
+
raise TypeError(Target)
|
968
|
+
self._target = target
|
969
|
+
|
970
|
+
self._cwd = cwd
|
971
|
+
self._python_path = python_path
|
972
|
+
self._sys_path = sys_path
|
973
|
+
self._os_exec = os_exec
|
974
|
+
|
975
|
+
@property
|
976
|
+
def target(self) -> Target:
|
977
|
+
return self._target
|
978
|
+
|
979
|
+
@property
|
980
|
+
def cwd(self): # type: () -> str | None
|
981
|
+
return self._cwd
|
982
|
+
|
983
|
+
@property
|
984
|
+
def python_path(self): # type: () -> list[str] | None
|
985
|
+
return self._python_path
|
986
|
+
|
987
|
+
@property
|
988
|
+
def sys_path(self): # type: () -> list[str] | None
|
989
|
+
return self._sys_path
|
990
|
+
|
991
|
+
@property
|
992
|
+
def os_exec(self) -> bool:
|
993
|
+
return self._os_exec
|
994
|
+
|
995
|
+
__attrs__ = (
|
996
|
+
'target',
|
997
|
+
'cwd',
|
998
|
+
'python_path',
|
999
|
+
'sys_path',
|
1000
|
+
'os_exec',
|
1001
|
+
)
|
1002
|
+
|
1003
|
+
def as_json(self): # type: () -> dict[str, object]
|
1004
|
+
return {
|
1005
|
+
'target': self._target.as_json(),
|
1006
|
+
'cwd': self._cwd,
|
1007
|
+
'python_path': self._python_path,
|
1008
|
+
'sys_path': self._sys_path,
|
1009
|
+
'os_exec': self._os_exec,
|
1010
|
+
}
|
1011
|
+
|
1012
|
+
|
1013
|
+
class ExecDecider:
|
1014
|
+
def __init__(
|
1015
|
+
self,
|
1016
|
+
env: RunEnv,
|
1017
|
+
exe: Exec,
|
1018
|
+
root_dir: str,
|
1019
|
+
*,
|
1020
|
+
debug_fn=None,
|
1021
|
+
) -> None:
|
1022
|
+
super().__init__()
|
1023
|
+
|
1024
|
+
self._env = env
|
1025
|
+
self._exe = exe
|
1026
|
+
self._root_dir = root_dir
|
1027
|
+
|
1028
|
+
self._debug_fn = debug_fn
|
1029
|
+
|
1030
|
+
def _debug(self, arg):
|
1031
|
+
if self._debug_fn is not None:
|
1032
|
+
self._debug_fn(arg)
|
1033
|
+
|
1034
|
+
def _filter_out_cwd(self, lst): # type: (list[str]) -> list[str]
|
1035
|
+
return [p for p in lst if p != self._env.cwd]
|
1036
|
+
|
1037
|
+
def _decide_file_target(self, tgt): # type: (Target) -> ExecDecision | None
|
1038
|
+
if not isinstance(tgt, FileTarget):
|
1039
|
+
return None
|
1040
|
+
|
1041
|
+
new_file = os.path.abspath(tgt.file)
|
1042
|
+
|
1043
|
+
return ExecDecision(
|
1044
|
+
tgt.replace(
|
1045
|
+
file=new_file,
|
1046
|
+
),
|
1047
|
+
cwd=self._root_dir,
|
1048
|
+
)
|
1049
|
+
|
1050
|
+
def _decide_module_target_not_in_root(self, tgt): # type: (Target) -> ExecDecision | None
|
1051
|
+
if not (isinstance(tgt, ModuleTarget) and self._env.cwd != self._root_dir):
|
1052
|
+
return None
|
1053
|
+
|
1054
|
+
rel_path = os.path.relpath(self._env.cwd, self._root_dir)
|
1055
|
+
new_mod = '.'.join([rel_path.replace(os.sep, '.'), tgt.module]) # noqa
|
1056
|
+
|
1057
|
+
return ExecDecision(
|
1058
|
+
tgt.replace(
|
1059
|
+
module=new_mod,
|
1060
|
+
),
|
1061
|
+
cwd=self._root_dir,
|
1062
|
+
os_exec=True,
|
1063
|
+
)
|
1064
|
+
|
1065
|
+
def _decide_debugger_file_target(self, tgt): # type: (Target) -> ExecDecision | None
|
1066
|
+
if not isinstance(tgt, DebuggerTarget):
|
1067
|
+
return None
|
1068
|
+
|
1069
|
+
dt = tgt.target
|
1070
|
+
if not (isinstance(dt, FileTarget) and dt.file.endswith('.py')):
|
1071
|
+
return None
|
1072
|
+
|
1073
|
+
af = os.path.abspath(dt.file)
|
1074
|
+
rp = os.path.relpath(af, self._root_dir).split(os.path.sep)
|
1075
|
+
mod = '.'.join([*rp[:-1], rp[-1][:-3]])
|
1076
|
+
new_dt = ModuleTarget(
|
1077
|
+
mod,
|
1078
|
+
dt.argv,
|
1079
|
+
)
|
1080
|
+
|
1081
|
+
return ExecDecision(
|
1082
|
+
tgt.replace(
|
1083
|
+
target=new_dt,
|
1084
|
+
),
|
1085
|
+
cwd=self._root_dir,
|
1086
|
+
python_path=self._filter_out_cwd(self._env.python_path),
|
1087
|
+
sys_path=self._filter_out_cwd(self._env.sys_path),
|
1088
|
+
)
|
1089
|
+
|
1090
|
+
def _decide_debugger_module_target_not_in_root(self, tgt): # type: (Target) -> ExecDecision | None
|
1091
|
+
if not (isinstance(tgt, DebuggerTarget) and self._env.cwd != self._root_dir):
|
1092
|
+
return None
|
1093
|
+
|
1094
|
+
dt = tgt.target
|
1095
|
+
if not isinstance(dt, ModuleTarget):
|
1096
|
+
return None
|
1097
|
+
|
1098
|
+
rp = os.path.relpath(self._env.cwd, self._root_dir).split(os.path.sep)
|
1099
|
+
mod = '.'.join([*rp, dt.module])
|
1100
|
+
new_dt = ModuleTarget(
|
1101
|
+
mod,
|
1102
|
+
dt.argv,
|
1103
|
+
)
|
1104
|
+
|
1105
|
+
return ExecDecision(
|
1106
|
+
tgt.replace(
|
1107
|
+
target=new_dt,
|
1108
|
+
),
|
1109
|
+
cwd=self._root_dir,
|
1110
|
+
python_path=self._filter_out_cwd(self._env.python_path),
|
1111
|
+
sys_path=self._filter_out_cwd(self._env.sys_path),
|
1112
|
+
)
|
1113
|
+
|
1114
|
+
def _decide_test_runner_target_not_in_root(self, tgt): # type: (Target) -> ExecDecision | None
|
1115
|
+
if not (isinstance(tgt, TestRunnerTarget) and self._env.cwd != self._root_dir):
|
1116
|
+
return None
|
1117
|
+
|
1118
|
+
def fix_test(t: Test) -> Test:
|
1119
|
+
if isinstance(t, PathTest):
|
1120
|
+
return PathTest(os.path.abspath(t.s))
|
1121
|
+
|
1122
|
+
elif isinstance(t, TargetTest):
|
1123
|
+
if ':' in t.s:
|
1124
|
+
l, _, r = t.s.partition(':')
|
1125
|
+
return TargetTest(':'.join([os.path.abspath(l), r]))
|
1126
|
+
else:
|
1127
|
+
return TargetTest(os.path.abspath(t.s))
|
1128
|
+
|
1129
|
+
else:
|
1130
|
+
raise TypeError(t)
|
1131
|
+
|
1132
|
+
new_tests = [fix_test(t) for t in tgt.tests]
|
1133
|
+
|
1134
|
+
return ExecDecision(
|
1135
|
+
tgt.replace(
|
1136
|
+
tests=new_tests,
|
1137
|
+
),
|
1138
|
+
cwd=self._root_dir,
|
1139
|
+
python_path=self._filter_out_cwd(self._env.python_path),
|
1140
|
+
sys_path=self._filter_out_cwd(self._env.sys_path),
|
1141
|
+
)
|
1142
|
+
|
1143
|
+
def _decide_debugger_test_runner_target_not_in_root(self, tgt): # type: (Target) -> ExecDecision | None
|
1144
|
+
if not isinstance(tgt, DebuggerTarget):
|
1145
|
+
return None
|
1146
|
+
|
1147
|
+
ne = self._decide_test_runner_target_not_in_root(tgt.target)
|
1148
|
+
if ne is None:
|
1149
|
+
return None
|
1150
|
+
|
1151
|
+
return ne.replace(
|
1152
|
+
target=tgt.replace(
|
1153
|
+
target=ne.target,
|
1154
|
+
),
|
1155
|
+
)
|
1156
|
+
|
1157
|
+
def decide(self, tgt): # type: (Target) -> ExecDecision | None
|
1158
|
+
for fn in [
|
1159
|
+
self._decide_file_target,
|
1160
|
+
self._decide_module_target_not_in_root,
|
1161
|
+
self._decide_debugger_file_target,
|
1162
|
+
self._decide_debugger_module_target_not_in_root,
|
1163
|
+
self._decide_test_runner_target_not_in_root,
|
1164
|
+
self._decide_debugger_test_runner_target_not_in_root,
|
1165
|
+
]:
|
1166
|
+
if (ne := fn(tgt)) is not None:
|
1167
|
+
self._debug(f'{fn.__name__=}')
|
1168
|
+
return ne
|
1169
|
+
|
1170
|
+
return None
|
1171
|
+
|
1172
|
+
|
1173
|
+
##
|
1174
|
+
|
1175
|
+
|
1176
|
+
class HackRunner:
|
1177
|
+
def __init__(
|
1178
|
+
self,
|
1179
|
+
*,
|
1180
|
+
is_debug: bool = False,
|
1181
|
+
is_enabled: bool = False,
|
1182
|
+
) -> None:
|
1183
|
+
super().__init__()
|
1184
|
+
|
1185
|
+
self._is_debug = is_debug
|
1186
|
+
self._is_enabled = is_enabled
|
1187
|
+
|
1188
|
+
def _debug(self, arg):
|
1189
|
+
if not self._is_debug:
|
1190
|
+
return
|
1191
|
+
|
1192
|
+
if isinstance(arg, str):
|
1193
|
+
s = arg
|
1194
|
+
else:
|
1195
|
+
try:
|
1196
|
+
import pprint # noqa
|
1197
|
+
except ImportError:
|
1198
|
+
s = repr(arg)
|
1199
|
+
else:
|
1200
|
+
s = pprint.pformat(arg, sort_dicts=False)
|
1201
|
+
|
1202
|
+
print(f'{_DEBUG_PREFIX}: {s}', file=sys.stderr)
|
1203
|
+
|
1204
|
+
@_cached_nullary
|
1205
|
+
def _env(self) -> RunEnv:
|
1206
|
+
return RunEnv()
|
1207
|
+
|
1208
|
+
@_cached_nullary
|
1209
|
+
def _root_dir(self): # type: () -> str | None
|
1210
|
+
env = self._env()
|
1211
|
+
|
1212
|
+
for d in [
|
1213
|
+
*env.ide_project_roots,
|
1214
|
+
*(env.sys_path or []),
|
1215
|
+
]:
|
1216
|
+
d = os.path.abspath(d)
|
1217
|
+
if os.path.isfile(os.path.join(d, 'pyproject.toml')):
|
1218
|
+
self._debug(f'root_dir={d!r}')
|
1219
|
+
return d
|
1220
|
+
|
1221
|
+
self._debug(f'not root dir')
|
1222
|
+
return None
|
1223
|
+
|
1224
|
+
@_cached_nullary
|
1225
|
+
def _exe(self) -> Exec:
|
1226
|
+
exe = parse_exec(self._env().orig_argv)
|
1227
|
+
self._debug(exe.as_json())
|
1228
|
+
return exe
|
1229
|
+
|
1230
|
+
@_cached_nullary
|
1231
|
+
def _decider(self) -> ExecDecider:
|
1232
|
+
return ExecDecider(
|
1233
|
+
self._env(),
|
1234
|
+
self._exe(),
|
1235
|
+
_check_not_none(self._root_dir()),
|
1236
|
+
debug_fn=self._debug,
|
1237
|
+
)
|
1238
|
+
|
1239
|
+
def _apply(self, dec: ExecDecision) -> None:
|
1240
|
+
if dec.cwd is not None:
|
1241
|
+
os.chdir(dec.cwd)
|
1242
|
+
|
1243
|
+
if dec.python_path is not None:
|
1244
|
+
os.environ['PYTHONPATH'] = os.pathsep.join(dec.python_path)
|
1245
|
+
|
1246
|
+
if dec.sys_path is not None:
|
1247
|
+
sys.path = dec.sys_path
|
1248
|
+
|
1249
|
+
if dec.os_exec:
|
1250
|
+
new_exe = self._exe().replace(
|
1251
|
+
target=dec.target,
|
1252
|
+
)
|
1253
|
+
|
1254
|
+
reexec_argv = render_exec_args(new_exe)
|
1255
|
+
self._debug(f'{reexec_argv=}')
|
1256
|
+
|
1257
|
+
os.execvp(reexec_argv[0], reexec_argv)
|
1258
|
+
|
1259
|
+
else:
|
1260
|
+
new_argv = render_target_args(dec.target)
|
1261
|
+
self._debug(new_argv)
|
1262
|
+
|
1263
|
+
sys.argv = new_argv
|
1264
|
+
|
1265
|
+
@_cached_nullary
|
1266
|
+
def run(self) -> None:
|
1267
|
+
# breakpoint()
|
1268
|
+
|
1269
|
+
env = self._env()
|
1270
|
+
self._debug(env.as_json())
|
1271
|
+
|
1272
|
+
if not self._is_enabled:
|
1273
|
+
self._debug('not enabled')
|
1274
|
+
return
|
1275
|
+
|
1276
|
+
if not self._root_dir():
|
1277
|
+
return
|
1278
|
+
|
1279
|
+
if not env.pycharm_hosted:
|
1280
|
+
self._debug('not pycharm hosted')
|
1281
|
+
return
|
1282
|
+
|
1283
|
+
exe = self._exe()
|
1284
|
+
dec = self._decider().decide(exe.target)
|
1285
|
+
if dec is None:
|
1286
|
+
self._debug('no decision')
|
1287
|
+
return
|
1288
|
+
|
1289
|
+
self._debug(dec.as_json())
|
1290
|
+
self._apply(dec)
|
1291
|
+
|
1292
|
+
|
1293
|
+
##
|
1294
|
+
|
1295
|
+
|
1296
|
+
_HAS_RUN = False
|
1297
|
+
|
1298
|
+
|
1299
|
+
def _run() -> None:
|
1300
|
+
global _HAS_RUN
|
1301
|
+
if _HAS_RUN:
|
1302
|
+
return
|
1303
|
+
_HAS_RUN = True
|
1304
|
+
|
1305
|
+
runner = HackRunner(
|
1306
|
+
is_debug=_get_opt_env_bool(DEBUG_ENV_VAR, _DEFAULT_DEBUG),
|
1307
|
+
is_enabled=_get_opt_env_bool(ENABLED_ENV_VAR, _DEFAULT_ENABLED),
|
1308
|
+
)
|
1309
|
+
|
1310
|
+
runner.run()
|
1311
|
+
|
1312
|
+
|
1313
|
+
##
|
1314
|
+
|
1315
|
+
|
1316
|
+
def _build_pth_file_src(module_name: str) -> str:
|
1317
|
+
return (
|
1318
|
+
'import sys; '
|
1319
|
+
r"exec('\n'.join(["
|
1320
|
+
"'try:', "
|
1321
|
+
f"' import {module_name}', "
|
1322
|
+
"'except ImportError:', "
|
1323
|
+
"' pass', "
|
1324
|
+
"'else:', "
|
1325
|
+
f"' {module_name}._run()'"
|
1326
|
+
"]))"
|
1327
|
+
)
|
1328
|
+
|
1329
|
+
|
1330
|
+
def _install_pth_file(
|
1331
|
+
*,
|
1332
|
+
file_name: str = _DEFAULT_PTH_FILE_NAME,
|
1333
|
+
module_name=None, # type: str | None
|
1334
|
+
dry_run: bool = False,
|
1335
|
+
editable: bool = False,
|
1336
|
+
force: bool = False,
|
1337
|
+
verbose: bool = False,
|
1338
|
+
) -> bool:
|
1339
|
+
import site
|
1340
|
+
lib_dir = site.getsitepackages()[0]
|
1341
|
+
verbose and print(f'{lib_dir=}')
|
1342
|
+
|
1343
|
+
pth_file = os.path.join(lib_dir, file_name)
|
1344
|
+
verbose and print(f'{pth_file=}')
|
1345
|
+
if not force and os.path.isfile(pth_file):
|
1346
|
+
verbose and print('pth_file exists, exiting')
|
1347
|
+
return False
|
1348
|
+
|
1349
|
+
if not editable:
|
1350
|
+
if module_name is None:
|
1351
|
+
module_name = '_' + file_name.removesuffix('.pth').replace('-', '_')
|
1352
|
+
verbose and print(f'{module_name=}')
|
1353
|
+
|
1354
|
+
mod_file = os.path.join(lib_dir, module_name + '.py')
|
1355
|
+
verbose and print(f'{mod_file=}')
|
1356
|
+
if not force and os.path.isfile(mod_file):
|
1357
|
+
verbose and print('mod_file exists, exiting')
|
1358
|
+
return False
|
1359
|
+
|
1360
|
+
import inspect
|
1361
|
+
mod_src = inspect.getsource(sys.modules[__name__])
|
1362
|
+
|
1363
|
+
else:
|
1364
|
+
if module_name is None:
|
1365
|
+
module_name = __package__ + '.runhack'
|
1366
|
+
verbose and print(f'{module_name=}')
|
1367
|
+
|
1368
|
+
mod_file = mod_src = None # type: ignore
|
1369
|
+
|
1370
|
+
pth_src = _build_pth_file_src(module_name)
|
1371
|
+
verbose and print(f'{pth_src=}')
|
1372
|
+
|
1373
|
+
if not dry_run:
|
1374
|
+
if mod_file is not None:
|
1375
|
+
verbose and print(f'writing {mod_file}')
|
1376
|
+
with open(mod_file, 'w') as f:
|
1377
|
+
f.write(mod_src) # type: ignore
|
1378
|
+
|
1379
|
+
verbose and print(f'writing {pth_file}')
|
1380
|
+
with open(pth_file, 'w') as f:
|
1381
|
+
f.write(pth_src)
|
1382
|
+
|
1383
|
+
return True
|
1384
|
+
|
1385
|
+
|
1386
|
+
if __name__ == '__main__':
|
1387
|
+
def _main() -> None:
|
1388
|
+
import argparse
|
1389
|
+
|
1390
|
+
parser = argparse.ArgumentParser()
|
1391
|
+
|
1392
|
+
subparsers = parser.add_subparsers()
|
1393
|
+
|
1394
|
+
def install_cmd(args):
|
1395
|
+
is_venv = sys.prefix != sys.base_prefix
|
1396
|
+
if not is_venv and not args.no_venv:
|
1397
|
+
raise RuntimeError('Refusing to run outside of venv')
|
1398
|
+
|
1399
|
+
success = _install_pth_file(
|
1400
|
+
dry_run=args.dry_run,
|
1401
|
+
editable=args.editable,
|
1402
|
+
force=args.force,
|
1403
|
+
verbose=args.verbose,
|
1404
|
+
)
|
1405
|
+
|
1406
|
+
sys.exit(0 if success else 1)
|
1407
|
+
|
1408
|
+
parser_install = subparsers.add_parser('install')
|
1409
|
+
parser_install.add_argument('--dry-run', action='store_true')
|
1410
|
+
parser_install.add_argument('-e', '--editable', action='store_true')
|
1411
|
+
parser_install.add_argument('-f', '--force', action='store_true')
|
1412
|
+
parser_install.add_argument('-v', '--verbose', action='store_true')
|
1413
|
+
parser_install.add_argument('--no-venv', action='store_true')
|
1414
|
+
parser_install.set_defaults(func=install_cmd)
|
1415
|
+
|
1416
|
+
args = parser.parse_args()
|
1417
|
+
if not getattr(args, 'func', None):
|
1418
|
+
parser.print_help()
|
1419
|
+
else:
|
1420
|
+
args.func(args)
|
1421
|
+
|
1422
|
+
_main()
|