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.
@@ -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()