omlish 0.0.0.dev66__py3-none-any.whl → 0.0.0.dev67__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,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()