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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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()