pyarmor.cli 9.2.6__3-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.
pyarmor/cli/project.py ADDED
@@ -0,0 +1,1281 @@
1
+ #! /usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ #############################################################
5
+ # #
6
+ # Copyright @ 2024 Dashingsoft corp. #
7
+ # All rights reserved. #
8
+ # #
9
+ # pyarmor #
10
+ # #
11
+ # Version: 9.1.0 - #
12
+ # #
13
+ #############################################################
14
+ #
15
+ #
16
+ # @File: pyarmor/cli/project.py
17
+ #
18
+ # @Author: Jondy Zhao(pyarmor@163.com)
19
+ #
20
+ # @Create Date: Tue Nov 12 16:38:51 CST 2024
21
+ #
22
+ # @Description:
23
+ #
24
+ # - Define project object for Pyarmor 9.
25
+ # - Define project commands: init, build
26
+ # - Define targets: std, mini, vmc, ecc, rft
27
+
28
+ """Manage projects
29
+
30
+ Config View
31
+ -----------
32
+
33
+ [project]
34
+ global_excludes = .* __pycache__
35
+ project_includes = *.py *.pyw
36
+ package_includes = *.py *.pyw
37
+ recursive = 0
38
+
39
+ name = str
40
+ src = absolute path
41
+
42
+ modules = pattern pattern ...
43
+ packages = pattern path@name @section ...
44
+ excludes = patterh patterh ...
45
+
46
+ pypaths = path modname::path
47
+
48
+ Examples
49
+ --------
50
+
51
+ 1. Create a project with scripts/packages in current path
52
+
53
+ $ pyarmor init -r
54
+
55
+ 2. Obfuscate all the scripts in the project
56
+
57
+ $ pyarmor build --rft
58
+ $ pyarmor build --mini
59
+
60
+ 3. Config project and print project information
61
+
62
+ $ pyarmor env -p
63
+ $ pyarmor env -p set rft:remove_docstr 1
64
+
65
+ """
66
+ import ast
67
+ import glob
68
+ import logging
69
+ import os
70
+ import tokenize
71
+
72
+ from collections import namedtuple
73
+ from fnmatch import fnmatch
74
+ from json import loads as jsonloads, load as jsonload
75
+ from os.path import (
76
+ abspath, basename, exists, isabs, join as joinpath,
77
+ normpath, relpath, splitext
78
+ )
79
+ from string import Template
80
+ from textwrap import dedent
81
+
82
+
83
+ logger = logging.getLogger('cli.build')
84
+
85
+
86
+ GRAPHVIZ_INDENT = ' '
87
+
88
+ ############################################################
89
+ #
90
+ # Project File View
91
+ #
92
+ ############################################################
93
+
94
+ GLOBAL_EXCLS = '.*', '__pycache__'
95
+ GLOBAL_INCLS = '*.py', '*.pyw'
96
+
97
+ ProjectItem = namedtuple(
98
+ 'ProjectItem',
99
+ ('name', 'src', 'scripts', 'modules', 'packages',
100
+ 'excludes', 'recursive'),
101
+ defaults=['', '', [], [], [], None, False]
102
+ )
103
+
104
+
105
+ def scan_path(path, includes=None, excludes=[], **options):
106
+ files, dirs = [], []
107
+ xlist = includes if includes else GLOBAL_INCLS
108
+ with os.scandir(path) as itdir:
109
+ for et in itdir:
110
+ if any([fnmatch(et.name, x) for x in excludes]):
111
+ continue
112
+ if et.is_dir(follow_symlinks=False):
113
+ dirs.append(et.name)
114
+ elif (et.is_file(follow_symlinks=False) and
115
+ any([fnmatch(et.name, x) for x in xlist])):
116
+ files.append(et.name)
117
+ return files, dirs
118
+
119
+
120
+ def search_item(root, pattern, excludes, recursive=0):
121
+ if not pattern:
122
+ return []
123
+
124
+ sep = os.sep if pattern.endswith(os.sep) else ''
125
+ result = []
126
+
127
+ pt = pattern if isabs(pattern) else joinpath(root, pattern)
128
+ for item in glob.glob(pt, recursive=recursive):
129
+ name = basename(item.strip(sep))
130
+ if excludes and any([fnmatch(name, x) for x in excludes]):
131
+ continue
132
+ result.append(item)
133
+ return [normpath(x) for x in result]
134
+
135
+
136
+ ############################################################
137
+ #
138
+ # Concepts
139
+ #
140
+ ############################################################
141
+
142
+ class Module:
143
+ """Module concept"""
144
+
145
+ def __init__(self, path, name=None, parent=None):
146
+ self.parent = parent
147
+ self._path = path
148
+ self._name = name if name else splitext(basename(path))[0]
149
+
150
+ self._co = None
151
+ self._tree = None
152
+ self._type = None
153
+
154
+ self._shebang = ''
155
+
156
+ @property
157
+ def name(self):
158
+ return '' if self._name == '__init__' else self._name
159
+
160
+ @property
161
+ def path(self):
162
+ return self._path
163
+
164
+ @property
165
+ def mtype(self):
166
+ return self._type
167
+
168
+ @property
169
+ def mtree(self):
170
+ return self._tree
171
+
172
+ @property
173
+ def qualname(self):
174
+ if isinstance(self.parent, (Project, type(None))):
175
+ return self._name
176
+ prefix = self.parent.qualname + ('.' if self.name else '')
177
+ return prefix + self.name
178
+
179
+ @property
180
+ def project(self):
181
+ """Return project this module belong to"""
182
+ return (self.parent if isinstance(self.parent, Project)
183
+ else self.parent.project)
184
+
185
+ @property
186
+ def abspath(self):
187
+ return (self._path if isabs(self._path) else
188
+ joinpath(self.parent.abspath, self._path))
189
+
190
+ @property
191
+ def destpath(self):
192
+ s = self.qualname + ('' if self.name else '.__init__')
193
+ return joinpath(*s.split('.')) + splitext(self.path)[-1]
194
+
195
+ @property
196
+ def shebang(self):
197
+ return self._shebang
198
+
199
+ def compile_file(self, force=False, optimize=-1):
200
+ if self._co is not None and not force:
201
+ return
202
+
203
+ self.parse_file(force=force)
204
+
205
+ options = {
206
+ 'optimize': optimize
207
+ }
208
+ logger.info('compile %s ...', self.qualname)
209
+ self._co = compile(self._tree, self.abspath, 'exec', **options)
210
+ logger.info('compile %s end', self.qualname)
211
+
212
+ def parse_file(self, force=False):
213
+ if self._tree is not None and not force:
214
+ return
215
+
216
+ filename = self.abspath
217
+ with open(filename, 'rb') as f:
218
+ encoding, lines = tokenize.detect_encoding(f.readline)
219
+ if lines and lines[0].startswith(b'#!'):
220
+ self._shebang = lines[0].decode(encoding)
221
+
222
+ with open(filename, 'r', encoding=encoding) as f:
223
+ logger.info('parse %s ...', self.qualname)
224
+ self._tree = ast.parse(f.read(), filename, 'exec')
225
+ logger.info('parse %s end', self.qualname)
226
+
227
+ def _as_dot(self):
228
+ return self.name
229
+
230
+
231
+ class Script(Module):
232
+ """Script concept"""
233
+
234
+ @property
235
+ def rpaths(self):
236
+ """Extra Python paths for importing module
237
+
238
+ If one script has any extra pypath, it need map imported
239
+ module name to project module name
240
+
241
+ For example, in the script `import abc`, maybe it uses module
242
+ `pkg.abc` in this project
243
+
244
+ This is only used by RFT mode, otherwise it doesn't know where
245
+ to find module `abc`
246
+
247
+ This property is used to generate internal `_mapped_modules`
248
+ """
249
+ pass
250
+
251
+
252
+ class Package(Module):
253
+ """Package concept"""
254
+
255
+ def __init__(self, path, name=None, parent=None, excludes=[]):
256
+ super().__init__(path, name=name, parent=parent)
257
+
258
+ self._modules = None
259
+ self._packages = None
260
+ self._excludes = excludes if excludes else GLOBAL_EXCLS
261
+ self._filters = None
262
+
263
+ @property
264
+ def filters(self):
265
+ if self._filters is None:
266
+ self._filters = []
267
+ for x in self._excludes:
268
+ i = x.find(':')
269
+ if i == -1:
270
+ self._filters.append(x)
271
+ elif fnmatch(self.qualname, x[:i]):
272
+ self._filters.append(x[i+1:])
273
+ return self._filters
274
+
275
+ def load(self):
276
+ excls = self.filters
277
+ files, dirs = scan_path(self.abspath, excludes=excls)
278
+ self._modules = [Module(x, parent=self) for x in files]
279
+ self._packages = [
280
+ Package(x, parent=self, excludes=self._excludes)
281
+ for x in dirs
282
+ ]
283
+
284
+ @property
285
+ def modules(self):
286
+ """Each package has many modules
287
+
288
+ There is one special module `__init__` for Package
289
+ """
290
+ if self._modules is None:
291
+ self.load()
292
+
293
+ for x in self._modules:
294
+ yield x
295
+
296
+ @property
297
+ def packages(self):
298
+ """Each package has many sub-packages"""
299
+ if self._packages is None:
300
+ self.load()
301
+
302
+ for x in self._packages:
303
+ yield x
304
+
305
+ def iter_module(self):
306
+ if self._modules is None or self._packages is None:
307
+ self.load()
308
+
309
+ for x in self._modules:
310
+ yield x
311
+
312
+ for pkg in self._packages:
313
+ for x in pkg.iter_module():
314
+ yield x
315
+
316
+ def _as_dot(self, n=0):
317
+ modules = [x._as_dot() for x in self.modules]
318
+ packages = [x._as_dot(n+1) for x in self.packages]
319
+ sep = '\n' + GRAPHVIZ_INDENT
320
+ source = Template(dedent("""\
321
+ subgraph cluster_$cid {
322
+ label="$name";
323
+ $modules
324
+ $packages
325
+ }""")).substitute(
326
+ cid=id(self),
327
+ name=self.name,
328
+ modules=sep.join(modules),
329
+ packages=sep.join(packages),
330
+ )
331
+ return (
332
+ ('\n' + GRAPHVIZ_INDENT * n).join(source.splitlines())
333
+ if n else source
334
+ )
335
+
336
+
337
+ class Namespace:
338
+ """Namespace concept"""
339
+
340
+ @property
341
+ def name(self):
342
+ pass
343
+
344
+ @property
345
+ def components(self):
346
+ """Each component has 3 items: path, modules, children
347
+
348
+ Each child is Namespace or Package
349
+ """
350
+ return []
351
+
352
+
353
+ class Project:
354
+ """Project conpect
355
+
356
+ Project is compose of Python elements and obfuscation settings
357
+
358
+ Each project has 4 components:
359
+
360
+ - Script
361
+ - Module
362
+ - Package
363
+ - Namespace
364
+
365
+ Each component has one unique name in this project except Script
366
+
367
+ It may has alias which also can't be duplicated with other names
368
+
369
+ Refer
370
+ -----
371
+
372
+ https://docs.python.org/3.13/reference/import.html#namespace-packages
373
+
374
+ """
375
+
376
+ ATTR_LOGFILE = '.pyarmor/project/rft_unknown_attrs.log'
377
+ CALL_LOGFILE = '.pyarmor/project/rft_unknown_calls.log'
378
+
379
+ def __init__(self, ctx):
380
+ self.ctx = ctx
381
+ self.src = ''
382
+
383
+ self._scripts = []
384
+ self._modules = []
385
+ self._packages = []
386
+ self._namespaces = []
387
+
388
+ self._rft_options = None
389
+ self._rft_filters = None
390
+ self._rft_rulers = None
391
+
392
+ self._rmodules = None
393
+ self._builtins = None
394
+
395
+ self._rft_type_rules = None
396
+ self._rft_include_attrs = None
397
+ self._used_external_types = None
398
+
399
+ self._std_options = None
400
+ self._mini_options = None
401
+ self._vmc_options = None
402
+ self._ecc_options = None
403
+
404
+ # Log variable name in chain attributes
405
+ #
406
+ # For example, in module "foo.py":
407
+ #
408
+ # def fa(x):
409
+ # x.runner[0].start()
410
+ #
411
+ # Because don't know the type of "x", log it as
412
+ #
413
+ # self.unknown_vars.append("foo:fa:x")
414
+ #
415
+ # self.unknown_vars = []
416
+
417
+ # Log attribute used but not defined in class
418
+ #
419
+ # For example, in module "foo.py":
420
+ #
421
+ # def fa(x: Fibo):
422
+ # x.items[0].run()
423
+ #
424
+ # If no found attribute "items" in class "Fibo", log it as
425
+ #
426
+ # self.unknown_attrs.append("foo:fa:x.items.run ?items")
427
+ #
428
+ self.unknown_attrs = set()
429
+
430
+ # Log function which called with **kwargs
431
+ #
432
+ # For example, in the module "foo.py":
433
+ #
434
+ # class Fibo:
435
+ #
436
+ # def runner(self, a=1, b=2):
437
+ # return a + b
438
+ #
439
+ # c = Fibo()
440
+ # c.runner(**data)
441
+ #
442
+ # Because the method "Fibo.runner" is called by dict
443
+ # argument "**data", log it as
444
+ #
445
+ # self.unknown_funcs.append("foo:Fibo.runner")
446
+ #
447
+ self.unknown_funcs = []
448
+
449
+ # Log unknown caller with keyword arguments.
450
+ #
451
+ # For example, in the module "foo.py":
452
+ #
453
+ # def fa(c):
454
+ # c.runner[2].echo(msg='hello')
455
+ #
456
+ # If don't know where "echo" is defined, log it as
457
+ #
458
+ # self.unknown_calls.append("foo:fa:c.runner.echo")
459
+ #
460
+ # If it uses dict arguments, for example:
461
+ #
462
+ # def fa(c):
463
+ # c.runner[2].echo(**data)
464
+ #
465
+ # Log it with suffix "*"
466
+ #
467
+ # self.unknown_calls.append("foo:fa:c.runner.echo*")
468
+ #
469
+ self.unknown_calls = None
470
+
471
+ # Log all attributes which is external base class attr
472
+ #
473
+ # For example:
474
+ #
475
+ # class C(dict):
476
+ #
477
+ # def merge(self, another):
478
+ # super().merge(another)
479
+ #
480
+ # "merge" will be loged to external_attrs
481
+ # self.external_attrs = []
482
+
483
+ @property
484
+ def abspath(self):
485
+ return abspath(self.src)
486
+
487
+ @property
488
+ def scripts(self):
489
+ """Project entry points
490
+
491
+ One project may has many entry points
492
+
493
+ Script can't be imported by other components
494
+ """
495
+ for x in self._scripts:
496
+ yield x
497
+
498
+ @property
499
+ def modules(self):
500
+ for x in self._modules:
501
+ yield x
502
+
503
+ @property
504
+ def packages(self):
505
+ """Only top packages"""
506
+ for x in self._packages:
507
+ yield x
508
+
509
+ @property
510
+ def namespaces(self):
511
+ """Only top namespace"""
512
+ for x in self._namespaces:
513
+ yield x
514
+
515
+ @property
516
+ def std_options(self):
517
+ """Options got from ctx.builder, not in project"""
518
+ if self._std_options is None:
519
+ cfg = self.ctx.cfg
520
+ optimize = cfg['builder'].getint('optimize', -1)
521
+ self._std_options = {
522
+ 'optimize': optimize
523
+ }
524
+ return self._std_options
525
+
526
+ def std_opt(self, name):
527
+ return self.std_options.get(name)
528
+
529
+ @property
530
+ def rft_options(self):
531
+ """Refactor options:
532
+
533
+ - remove_assert: bool
534
+
535
+ If 1, remove assert node
536
+
537
+ - remove_docstr: bool
538
+
539
+ If 1, remove all docstring
540
+
541
+ - builtin_mode: bool
542
+
543
+ 0, do not touch any builtin names
544
+ 1, as build target, maybe std, mini or plain
545
+
546
+ - rft_import: bool
547
+
548
+ always 1
549
+
550
+ - rft_ximport: bool
551
+
552
+ Always 0, do not touch node "from..import *"
553
+
554
+ - argument_mode: enum('0', '1', '2', '3')
555
+
556
+ 0: "no", no reform any argument node
557
+ 1: "pos", reform posonly arguments
558
+ 2: '!kw', no reform keyword only arguments
559
+ 3: "all", reform all arguments
560
+
561
+ Note that if function is exported, no arguments reformed
562
+
563
+ - obf_attribute: enum(no, yes, all)
564
+
565
+ Reform attribute node to setattr() or getattr()
566
+
567
+ 0, do not reform attribute to setattr or getattr
568
+ 1, reform attribute node by rft_attribute_filters
569
+ 2, reform all attribute node
570
+
571
+ - obf_string: enum(no, yes, all)
572
+
573
+ Reform string constant to security mode
574
+
575
+ It only works for std/mini target
576
+
577
+ It's always 0 for plain target
578
+
579
+ 0, no reform string
580
+ 1, reform string by rft_string_filters
581
+ 2, reform all string
582
+
583
+ - export_mode: bool
584
+
585
+ True: auto export all names in module.__all__
586
+
587
+ If class is exported, all class members are exported
588
+ If function is exported, arguments can't be renamed
589
+ Only module variable can be exported separately
590
+
591
+ - exclude_names: list
592
+
593
+ Move it from rft_filter
594
+
595
+ - exclude_funcs: list
596
+
597
+ Move it from rft_filter
598
+
599
+ - rft_str_keywords: list (not implemented)
600
+
601
+ Rename string constant or key in dict constant
602
+
603
+ When call function, solve argument not found issue
604
+
605
+ - rft_type_rules: dict
606
+
607
+ Specify variable type
608
+
609
+ - extra_type_info: dict
610
+
611
+ Specify extra attribute for module or type
612
+
613
+ - wildcard_import_table: dict (unused now)
614
+
615
+ When building target, it need import module to get name
616
+ for wildcard import, sometimes it may failed
617
+
618
+ In order to avoid importing module in build time, users
619
+ can provide all names in wildcard imported module
620
+
621
+ - extra_builtins: list
622
+
623
+ By default, builtin names is got from builtin module
624
+
625
+ User can append extra builtin names
626
+
627
+ - on_unknown_attr: enum(ask, log, yes, no, err)
628
+
629
+ When don't know how to refactor attribute
630
+
631
+ ask: query user interactively
632
+ log: no touch attr but log it
633
+ yes: rename attr
634
+ no: do thing
635
+ err: raise error
636
+ """
637
+ if self._rft_options is None:
638
+ cfg = self.ctx.cfg
639
+ sect = 'rft'
640
+ if cfg.has_section(sect):
641
+ self._rft_options = dict(cfg.items(sect))
642
+ else:
643
+ self._rft_options = {}
644
+ return self._rft_options
645
+
646
+ def rft_opt(self, name):
647
+ return self.rft_options.get(name)
648
+
649
+ @property
650
+ def rft_exclude_names(self):
651
+ """Exclude module, class, function
652
+
653
+ All names in this scope aren't renamed. For example,
654
+
655
+ Exclude module, all classes and functions aren't renamed
656
+ Exclude class, all class attributes aren't renamed
657
+
658
+ Each ruler is one chained names
659
+ Each ruler must start with package or module name
660
+ It supports pattern match as fnmatchcase
661
+ Pattern only match one level
662
+ """
663
+ value = self.rft_opt('exclude_names')
664
+ if value:
665
+ for x in value.splitlines():
666
+ yield x
667
+
668
+ @property
669
+ def rft_exclude_funcs(self):
670
+ """No touch arguments for listed functions"""
671
+ value = self.rft_opt('exclude_funcs')
672
+ if value:
673
+ for x in value.splitlines():
674
+ yield x
675
+
676
+ @property
677
+ def rft_filters(self):
678
+ if self._rft_filters is None:
679
+ cfg = self.ctx.cfg
680
+ sect = 'rft_filter'
681
+ if cfg.has_section(sect):
682
+ self._rft_filters = dict(cfg.items(sect))
683
+ else:
684
+ self._rft_filters = {}
685
+ return self._rft_filters
686
+
687
+ @property
688
+ def obf_include_strings(self):
689
+ """A list of re pattern based on obf_string
690
+
691
+ All matched string in ast.Tree will be transformed
692
+ """
693
+ value = self.rft_filters.get('obf_include_strings', '')
694
+ for x in value.splitlines():
695
+ yield x
696
+
697
+ @property
698
+ def obf_attr_filters(self):
699
+ """A list of re pattern based on obf_attribute
700
+
701
+ All matched ast.Attribute will be transformed to call
702
+ setattr() or getattr() to hide attribute name
703
+ """
704
+ value = self.rft_filters.get('obf_attr_filters', '')
705
+ for x in value.splitlines():
706
+ yield x
707
+
708
+ @property
709
+ def rft_rulers(self):
710
+ if self._rft_rulers is None:
711
+ cfg = self.ctx.cfg
712
+ sect = 'rft_ruler'
713
+ if cfg.has_section(sect):
714
+ self._rft_rulers = dict(cfg.items(sect))
715
+ else:
716
+ self._rft_rulers = {}
717
+ return self._rft_rulers
718
+
719
+ @property
720
+ def rft_attr_rules(self):
721
+ """Refactor attribute rules, for special attribute node
722
+
723
+ If can't decide variable type, use rule for chains
724
+
725
+ For example, "x.a.b", if "x" of type is unknown
726
+
727
+ Use rule "x.a.b" to rename attribute "a", "b"
728
+
729
+ Use rule "x.a.b.c" to rename "a", "b", "c"
730
+
731
+ Use rule "x.a.b -> *.?.*" to rename "a" only
732
+
733
+ Use rule "*.write -> *.write" to keep all write attribute
734
+ """
735
+ value = self.rft_options.get('attr_rules', '')
736
+ for x in value.splitlines():
737
+ yield x
738
+
739
+ @property
740
+ def rft_call_rules(self):
741
+ """Refactor keyword argument in call statement
742
+
743
+ If can't decide function type, use ruler to rename arg
744
+ """
745
+ value = self.rft_options.get('call_rules', '')
746
+ for x in value.splitlines():
747
+ yield x
748
+
749
+ @property
750
+ def rft_arg_rules(self):
751
+ """Refactor rule, for arg name in Function/Call node
752
+
753
+ For example, in the call statement
754
+
755
+ kwargs = { 'msg': 'hello' }
756
+ foo(**kwargs)
757
+
758
+ This kind of rule could be used to rename string `msg`
759
+ """
760
+ value = self.rft_options.get('rft_arg_rules', '')
761
+ for x in value.splitlines():
762
+ yield x
763
+
764
+ @property
765
+ def rft_type_rules(self):
766
+ """Specify variable/blockvar type
767
+
768
+ Support format:
769
+
770
+ modname:scope:var typename
771
+ modname:scope:var.[?] typename
772
+ modname:scope:func.() typename
773
+ modname:scope:cls.method.() typename
774
+
775
+ For blockvar in For/Comprehension/With
776
+ modname:scope:{var} typename
777
+ """
778
+ if self._rft_type_rules is None:
779
+ vartypes = {}
780
+ lines = self.rft_opt('var_types')
781
+ for line in lines.splitlines() if lines else []:
782
+ varinfo, tname = line.split()
783
+ if varinfo.endswith('}'):
784
+ info = varinfo[:-1].replace('{', '')
785
+ modname, varname = info.split(':', 1)
786
+ vartypes.setdefault(modname, {})
787
+ mtypes = vartypes[modname]
788
+ mtypes.setdefault('__ivars__', {})
789
+ mtypes['__ivars__'][varname] = tname
790
+ else:
791
+ modname, varname = varinfo.split(':', 1)
792
+ vartypes.setdefault(modname, {})
793
+ vartypes[modname][varname] = tname
794
+ self._rft_type_rules = vartypes
795
+ return self._rft_type_rules
796
+
797
+ @property
798
+ def mini_options(self):
799
+ """MINI mode options:
800
+
801
+ - import_from: str
802
+
803
+ Default value is "pyarmor.mini.pyarmor_mini", it means from
804
+ package `pyarmor.mini` import extension `pyarmor_mini`
805
+
806
+ Set it to "pyarmor_mini" if it's distributed with obfuscated
807
+ scripts
808
+ """
809
+ if self._mini_options is None:
810
+ cfg = self.ctx.cfg
811
+ sect = 'mini'
812
+ if cfg.has_section(sect):
813
+ self._mini_options = dict(cfg.items(sect))
814
+ else:
815
+ self._mini_options = {}
816
+ self._mini_options.setdefault(
817
+ 'import_from', 'pyarmor.mini.pyarmor_mini')
818
+ return self._mini_options
819
+
820
+ def mini_opt(self, name):
821
+ return self.mini_options.get(name)
822
+
823
+ @property
824
+ def vmc_options(self):
825
+ """VMC mode options"""
826
+ if self._vmc_options is None:
827
+ cfg = self.ctx.cfg
828
+ sect = 'vmc'
829
+ if cfg.has_section(sect):
830
+ self._vmc_options = dict(cfg.items(sect))
831
+ else:
832
+ self._vmc_options = {}
833
+ return self._vmc_options
834
+
835
+ def vmc_opt(self, name):
836
+ return self.vmc_options.get(name)
837
+
838
+ @property
839
+ def ecc_options(self):
840
+ """ECC mode options"""
841
+ if self._ecc_options is None:
842
+ cfg = self.ctx.cfg
843
+ sect = 'ecc'
844
+ if cfg.has_section(sect):
845
+ self._ecc_options = dict(cfg.items(sect))
846
+ else:
847
+ self._ecc_options = {}
848
+ return self._ecc_options
849
+
850
+ def ecc_opt(self, name):
851
+ return self.ecc_options.get(name)
852
+
853
+ @property
854
+ def builtins(self):
855
+ if self._builtins is None:
856
+ import builtins
857
+ self._builtins = set(dir(builtins))
858
+ return self._builtins
859
+
860
+ @property
861
+ def used_external_types(self):
862
+ """Used external types"""
863
+ if self._used_external_types is None:
864
+ used_types = {}
865
+ names = 'builtins'
866
+ for name in names.split():
867
+ if name.endswith('.json'):
868
+ if exists(name):
869
+ with open(name, encoding='utf-8') as f:
870
+ used_types.update(jsonload(f))
871
+ else:
872
+ modname = name.split('::')[0]
873
+ alltypes = self._get_external_types(modname)
874
+ if alltypes:
875
+ used_types.update(alltypes)
876
+ self._used_external_types = used_types
877
+ return self._used_external_types
878
+
879
+ @property
880
+ def rft_external_types(self):
881
+ """External types manual"""
882
+ value = self.rft_options.get('external_types', '')
883
+ if value:
884
+ for x in value.split():
885
+ yield x
886
+
887
+ @property
888
+ def rft_external_attrs(self):
889
+ """External attrs manual
890
+ x means append to attrs of rft_external_types
891
+ !x means exclude x from attrs of rft_external_types
892
+ """
893
+ value = self.rft_options.get('external_attrs', '')
894
+ if value:
895
+ for x in value.split():
896
+ yield x
897
+
898
+ @property
899
+ def rft_include_attrs(self):
900
+ """List attributes need to be renamed on unknown type"""
901
+ if self._rft_include_attrs is None:
902
+ self._rft_include_attrs = set()
903
+ value = self.rft_options.get('include_attrs', '')
904
+ if value:
905
+ self._rft_include_attrs.update(value.split())
906
+ return self._rft_include_attrs
907
+
908
+ def get_module(self, qualname):
909
+ """Get module in the project by unique qualname
910
+ It equals one dict: map_qualname_to_module
911
+ """
912
+ if self._rmodules is None:
913
+ self._rmodules = {
914
+ x.qualname: x for x in self.iter_module()
915
+ }
916
+ return self._rmodules.get(qualname)
917
+
918
+ def iter_module(self):
919
+ """Iterate all modules in this project"""
920
+ for x in self._scripts:
921
+ yield x
922
+
923
+ for x in self._modules:
924
+ yield x
925
+
926
+ for child in self._packages + self._namespaces:
927
+ for x in child.iter_module():
928
+ yield x
929
+
930
+ def relsrc(self, path):
931
+ return relpath(path, self.src)
932
+
933
+ def load(self, data):
934
+ """Init project object with dict
935
+
936
+ It equals:
937
+
938
+ 1. map init data to ProjectItem
939
+ 2. map ProjectItem to project files
940
+ """
941
+ dp = joinpath(self.ctx.local_path, 'project')
942
+ os.makedirs(dp, exist_ok=True)
943
+
944
+ def vlist(name):
945
+ return [x.strip().replace('%20%', ' ')
946
+ for x in data.get(name, '').split()]
947
+
948
+ src = self.src = data['src']
949
+ name = data.get('name')
950
+ excludes = vlist('excludes') + list(GLOBAL_EXCLS)
951
+ proexcls = [x.strip(':') for x in excludes
952
+ if x.find(':') < 1]
953
+
954
+ scripts = []
955
+ for pat in vlist('scripts'):
956
+ scripts.extend(search_item(src, pat, proexcls))
957
+ self._scripts.extend([
958
+ Script(self.relsrc(x), parent=self) for x in scripts
959
+ ])
960
+
961
+ modules = []
962
+ for pat in vlist('modules'):
963
+ modules.extend(search_item(src, pat, proexcls))
964
+
965
+ packages = vlist('packages')
966
+ if packages:
967
+ for item in packages:
968
+ # 3 forms: path, path@name, @sect
969
+ i = item.find('@')
970
+ if i == 0:
971
+ raise NotImplementedError(f'package {item}')
972
+ if i == -1:
973
+ path, pkgname = item, basename(item)
974
+ else:
975
+ path, pkgname = item.split('@')
976
+ if not isabs(path):
977
+ path = joinpath(src, path)
978
+ obj = Package(path,
979
+ name=pkgname,
980
+ parent=self,
981
+ excludes=excludes)
982
+ self._packages.append(obj)
983
+
984
+ recursive = data.get('recursive', '0')
985
+ if recursive == '1':
986
+ pkginit = joinpath(src, '__init__.py')
987
+ if exists(pkginit):
988
+ pkgname = name if name else basename(src)
989
+ obj = Package(src,
990
+ name=pkgname,
991
+ parent=self,
992
+ excludes=excludes)
993
+ self._packages.append(obj)
994
+ else:
995
+ files, dirs = scan_path(src, excludes=proexcls)
996
+ modules.extend([joinpath(src, x) for x in files])
997
+ self._packages.extend([
998
+ Package(x, parent=self) for x in dirs
999
+ ])
1000
+
1001
+ if scripts and modules:
1002
+ for x in set(scripts) & set(modules):
1003
+ logger.debug('duplicated %s', self.relsrc(x))
1004
+ modules.remove(x)
1005
+ self._modules.extend([
1006
+ Module(self.relsrc(x), parent=self) for x in modules
1007
+ ])
1008
+
1009
+ logger.info('load %d scripts', len(self._scripts))
1010
+ logger.info('load %d modules', len(self._modules))
1011
+ logger.info('load %d packages', len(self._packages))
1012
+
1013
+ def start(self):
1014
+ self._logfile = open(self.ATTR_LOGFILE, 'w')
1015
+ self._logfile2 = open(self.CALL_LOGFILE, 'w')
1016
+
1017
+ def stop(self):
1018
+ self._logfile.close()
1019
+ self._logfile2.close()
1020
+
1021
+ def log_unknown_attr(self, line):
1022
+ fields = line.split(':')
1023
+ attrs = fields[2].split('.')
1024
+ start = int(fields[3])
1025
+ self.unknown_attrs.update([
1026
+ x for x in attrs[start:] if x[:1] not in ('(', '[')
1027
+ ])
1028
+ self._logfile.write(line + '\n')
1029
+
1030
+ def log_unknown_func(self, func):
1031
+ if func not in self.unknown_funcs:
1032
+ self.unknown_funcs.append(func)
1033
+
1034
+ def log_unknown_call(self, line):
1035
+ self.unknown_calls = True
1036
+ self._logfile2.write(line + '\n')
1037
+
1038
+ def _get_external_types(self, modname, pypaths=None):
1039
+ from sys import executable
1040
+ from subprocess import check_output, CalledProcessError, DEVNULL
1041
+ source = Template(dedent("""\
1042
+ import json
1043
+ import sys
1044
+ x = ${pypaths}
1045
+ if x:
1046
+ sys.path[0:0] = [x] if isinstance(x, str) else x
1047
+ import ${name}
1048
+ typeinfo = {'${name}': []}
1049
+ for key, value in ${name}.__dict__.items():
1050
+ if key[:2] == '__':
1051
+ continue
1052
+ if isinstance(value, type):
1053
+ typeinfo['${name}.%s' % key] = [
1054
+ x for x in dir(value) if x[:2] != '__'
1055
+ ]
1056
+ typeinfo['${name}'].append(key)
1057
+ print(json.dumps(typeinfo))
1058
+ """)).substitute(name=modname, pypaths=repr(pypaths))
1059
+
1060
+ try:
1061
+ output = check_output([executable, '-c', source], stderr=DEVNULL)
1062
+ return jsonloads(output)
1063
+ except CalledProcessError:
1064
+ pass
1065
+
1066
+ def get_external_type(self, modname, clsname=None):
1067
+ if not all([x.isidentifier() for x in modname.split('.')]):
1068
+ logger.debug('invalid external module: %s', modname)
1069
+ return []
1070
+
1071
+ extypes = self.used_external_types
1072
+ if modname not in extypes:
1073
+ result = self._get_external_types(modname)
1074
+ if result:
1075
+ extypes.update(result)
1076
+ if clsname is None:
1077
+ return extypes
1078
+ return extypes.get('%s.%s' % (modname, clsname), [])
1079
+
1080
+ def preview_autofix_result(self, mode):
1081
+ if mode not in (2, 3):
1082
+ return
1083
+ output = f'.pyarmor/project/rft_autofix.{mode}.org'
1084
+
1085
+ rulefile = '.pyarmor/project/rft_autofix.rules'
1086
+ if not exists(rulefile):
1087
+ logger.info('no found %s', rulefile)
1088
+ return
1089
+ with open(rulefile, encoding='utf-8') as f:
1090
+ fixtable = jsonload(f)
1091
+
1092
+ if mode == 2:
1093
+ self._preview_autofix_2(fixtable, output)
1094
+ elif mode == 3:
1095
+ self._preview_autofix_3(fixtable, output)
1096
+
1097
+ def _preview_autofix_2(self, fixtable, output):
1098
+ modules = fixtable['modules']
1099
+ attrinfo = {}
1100
+ with open(self.ATTR_LOGFILE) as f:
1101
+ # Line format:
1102
+ # modname:scopes:attrs:index:begin_line,end_line
1103
+ for line in f:
1104
+ line = line.strip()
1105
+ fields = line.split(':')
1106
+ modname, scope = fields[:2]
1107
+ attrs = [x for x in fields[2].split('.')
1108
+ if x[:1] not in ('(', '[')]
1109
+ start = int(fields[3])
1110
+ lineno = [int(x) for x in fields[4].split(',')]
1111
+ for s in attrs[start:]:
1112
+ attrinfo.setdefault(s, [])
1113
+ fname = modules.get(modname, modname)
1114
+ title = ':'.join([
1115
+ modname, scope, '.'.join(attrs)
1116
+ ])
1117
+ attrinfo[s].append([fname, lineno[0], title])
1118
+
1119
+ header = Template(dedent("""\
1120
+ * RFT AutoFix Mode 2
1121
+
1122
+ The following attributes have been renamed, if any of
1123
+ them should not be renamed, configure it as external
1124
+ attribute and run autofix mode again.
1125
+
1126
+ For example, suppose `append` is external attribute:
1127
+
1128
+ pyarmor env -p push rft:external_attrs append
1129
+ pyarmor build --autofix 2
1130
+ pyarmor build --rft
1131
+
1132
+ Refactoring Attribute List
1133
+
1134
+ $attrs
1135
+
1136
+ """))
1137
+ attrsect = Template(dedent("""\
1138
+ ** $name
1139
+
1140
+ $srclinks
1141
+
1142
+ """))
1143
+
1144
+ inattrs = fixtable.get('include_attrs', [])
1145
+ rftattrs = header.substitute(attrs='\n'.join(
1146
+ [f'[[*{x}][{x}]]' for x in inattrs]))
1147
+ with open(output, 'w', encoding='utf-8') as fp:
1148
+ fp.write(rftattrs)
1149
+ for attr in inattrs:
1150
+ infos = attrinfo.get(attr)
1151
+ if infos is None:
1152
+ continue
1153
+ fp.write(attrsect.substitute(
1154
+ name=attr,
1155
+ srclinks='\n'.join([
1156
+ f'- [[file:{fname}::{n}][{title}]]'
1157
+ for fname, n, title in infos
1158
+ ])
1159
+ ))
1160
+
1161
+ def _preview_autofix_3(self, fixtable, output):
1162
+ modules = fixtable['modules']
1163
+ confused_names = fixtable.get('confused_names', [])
1164
+ attrules = {}
1165
+ rftattrs = {}
1166
+ with open(self.ATTR_LOGFILE) as f:
1167
+ # Line format:
1168
+ # modname:scopes:attrs:index:begin_line,end_line
1169
+ for line in f:
1170
+ fields = line.strip().split(':')
1171
+ modname, scope = fields[:2]
1172
+ attrs = [x for x in fields[2].split('.')
1173
+ if x[:1] not in ('(', '[')]
1174
+ start = int(fields[3])
1175
+ lineno = fields[4].split(',')[0]
1176
+ actions = ['*'] * start
1177
+ rnames = []
1178
+ for s in attrs[start:]:
1179
+ if s in confused_names:
1180
+ actions.append('?')
1181
+ rnames.append(s)
1182
+ else:
1183
+ actions.append('*')
1184
+ if rnames:
1185
+ aname = '.'.join(attrs)
1186
+ pat = '.'.join(actions)
1187
+ filename = modules.get(modname, modname)
1188
+ rule = f'{modname}::{scope}:{aname} {pat}'
1189
+ anchor = modname, filename, lineno
1190
+ if rule not in attrules:
1191
+ attrules[rule] = [anchor]
1192
+ else:
1193
+ attrules[rule].append(anchor)
1194
+ s = rnames[0]
1195
+ rftattrs.setdefault(s, set())
1196
+ rftattrs[s].add(rule)
1197
+
1198
+ header = Template(dedent("""\
1199
+ * RFT AutoFix Mode 3
1200
+
1201
+ The following attributes have been renamed, if any of
1202
+ them should not be renamed, add one attribute rule and
1203
+ run autofix mode again.
1204
+
1205
+ For example, there is one line
1206
+
1207
+ *** rftbuild::log_patch_script:cfgpatch.append *.?
1208
+ - rftbuild.py::2690
1209
+
1210
+ But `cfgpatch` is builtin type `list`, and its attribute
1211
+ `append` shouldn't be renamed
1212
+
1213
+ In this case, replace `?` with `*`, add one rule:
1214
+
1215
+ pyarmor env -p push rft:attr_rules "rftbuild::log_patch_script:cfgpatch.append *.*"
1216
+
1217
+ Then rebuild the project:
1218
+
1219
+ pyarmor build --autofix 3
1220
+ pyarmor build --rft
1221
+
1222
+ Refactoring Attribute List
1223
+
1224
+ $attrs
1225
+
1226
+ """))
1227
+ attrsect = Template(dedent("""\
1228
+ ** $name
1229
+
1230
+ $rules
1231
+
1232
+ """))
1233
+
1234
+ rulesect = Template(dedent("""\
1235
+ *** $rule
1236
+ $srclinks
1237
+ """))
1238
+
1239
+ attrs = sorted(rftattrs.keys())
1240
+ with open(output, 'w', encoding='utf-8') as fp:
1241
+ fp.write(header.substitute(
1242
+ attrs='\n'.join([f'[[*{x}][{x}]]' for x in attrs])
1243
+ ))
1244
+ for key in attrs:
1245
+ rules = rftattrs[key]
1246
+ rlist = []
1247
+ for r in rules:
1248
+ infos = attrules[r]
1249
+ rlist.append(rulesect.substitute(
1250
+ rule=r,
1251
+ srclinks='\n'.join([
1252
+ f'- [[file:{s}::{n}][{m}:{n}]]'
1253
+ for m, s, n in infos
1254
+ ])
1255
+ ))
1256
+ fp.write(attrsect.substitute(
1257
+ name=key,
1258
+ rules='\n'.join(rlist)
1259
+ ))
1260
+
1261
+ def _as_dot(self):
1262
+ """Map project to dot graph"""
1263
+ modules = [x._as_dot() for x in self.modules]
1264
+ packages = [x._as_dot() for x in self.packages]
1265
+ sep = '\n' + GRAPHVIZ_INDENT * 2
1266
+ return Template(dedent("""\
1267
+ graph {
1268
+ layout=osage
1269
+ subgraph cluster_0 {
1270
+ label="Project Structure";
1271
+ $modules
1272
+ $packages
1273
+ }
1274
+ }""")).substitute(
1275
+ modules=sep.join(modules),
1276
+ packages=sep.join('\n'.join(packages).splitlines())
1277
+ )
1278
+
1279
+
1280
+ if __name__ == '__main__':
1281
+ pass