omlish 0.0.0.dev66__py3-none-any.whl → 0.0.0.dev67__py3-none-any.whl

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