pyarmor.cli 9.2.5__2-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/__init__.py +46 -0
- pyarmor/cli/__main__.py +812 -0
- pyarmor/cli/batch.py +62 -0
- pyarmor/cli/bootstrap.py +207 -0
- pyarmor/cli/bug.py +137 -0
- pyarmor/cli/command.py +563 -0
- pyarmor/cli/config.py +306 -0
- pyarmor/cli/context.py +790 -0
- pyarmor/cli/core.data.1 +0 -0
- pyarmor/cli/core.data.2 +0 -0
- pyarmor/cli/core.data.3 +0 -0
- pyarmor/cli/default.cfg +564 -0
- pyarmor/cli/docker.py +192 -0
- pyarmor/cli/generate.py +256 -0
- pyarmor/cli/hdinfo.py +81 -0
- pyarmor/cli/merge.py +232 -0
- pyarmor/cli/mixer.py +109 -0
- pyarmor/cli/model.py +460 -0
- pyarmor/cli/plugin.py +292 -0
- pyarmor/cli/project.py +1281 -0
- pyarmor/cli/public_capsule.zip +0 -0
- pyarmor/cli/register.py +1093 -0
- pyarmor/cli/repack.py +793 -0
- pyarmor/cli/resource.py +265 -0
- pyarmor/cli/shell.py +481 -0
- pyarmor_cli-9.2.5.dist-info/METADATA +74 -0
- pyarmor_cli-9.2.5.dist-info/RECORD +30 -0
- pyarmor_cli-9.2.5.dist-info/WHEEL +6 -0
- pyarmor_cli-9.2.5.dist-info/entry_points.txt +3 -0
- pyarmor_cli-9.2.5.dist-info/top_level.txt +1 -0
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
|