obstruct 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- obstruct-0.2.4/.idea/modules.xml +8 -0
- obstruct-0.2.2/.idea/Hydrogenlib-Objective-Struct.iml → obstruct-0.2.4/.idea/obstruct.iml +1 -4
- {obstruct-0.2.2 → obstruct-0.2.4}/.idea/workspace.xml +49 -21
- {obstruct-0.2.2 → obstruct-0.2.4}/PKG-INFO +32 -9
- {obstruct-0.2.2 → obstruct-0.2.4}/README.md +31 -8
- obstruct-0.2.4/src/obstruct/__version__.py +1 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/aot/base.py +17 -12
- obstruct-0.2.4/src/obstruct/aot/pack.py +80 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/aot/unpack.py +43 -27
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/methods.py +1 -1
- obstruct-0.2.4/test/test_speed.py +95 -0
- obstruct-0.2.2/.idea/modules.xml +0 -8
- obstruct-0.2.2/src/obstruct/__version__.py +0 -1
- obstruct-0.2.2/src/obstruct/aot/pack.py +0 -58
- obstruct-0.2.2/test/test_speed.py +0 -59
- {obstruct-0.2.2 → obstruct-0.2.4}/.idea/.gitignore +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/.idea/inspectionProfiles/Project_Default.xml +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/.idea/misc.xml +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/.idea/vcs.xml +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/pyproject.toml +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/__init__.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/aot/__init__.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/aot/aot.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/decorator.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/struct_type.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/types/__init__.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/types/base.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/types/types.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/src/obstruct/types/types.pyi +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/test/test.py +0 -0
- {obstruct-0.2.2 → obstruct-0.2.4}/uv.lock +0 -0
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
|
|
3
3
|
<component name="NewModuleRootManager">
|
|
4
4
|
<content url="file://$MODULE_DIR$">
|
|
5
|
-
<sourceFolder url="file://$MODULE_DIR$/src" />
|
|
5
|
+
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
|
6
6
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
|
7
7
|
</content>
|
|
8
8
|
<orderEntry type="jdk" jdkName="E:\.projects\obstruct\.venv" jdkType="Python SDK" />
|
|
9
9
|
<orderEntry type="sourceFolder" forTests="false" />
|
|
10
10
|
</component>
|
|
11
|
-
<component name="PackageRequirementsSettings" />
|
|
12
11
|
<component name="PyDocumentationSettings">
|
|
13
12
|
<option name="format" value="PLAIN" />
|
|
14
13
|
</component>
|
|
15
|
-
<component name="ReSTService" />
|
|
16
|
-
<component name="TestRunnerService" />
|
|
17
14
|
</module>
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
5
|
</component>
|
|
6
6
|
<component name="ChangeListManager">
|
|
7
|
-
<list default="true" id="b482b6b4-48ce-4be1-bf3c-a72ec254fa35" name="更改" comment="
|
|
7
|
+
<list default="true" id="b482b6b4-48ce-4be1-bf3c-a72ec254fa35" name="更改" comment="修正了一些命名">
|
|
8
8
|
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
|
9
9
|
<change beforePath="$PROJECT_DIR$/src/obstruct/aot/base.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/obstruct/aot/base.py" afterDir="false" />
|
|
10
|
+
<change beforePath="$PROJECT_DIR$/src/obstruct/aot/pack.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/obstruct/aot/pack.py" afterDir="false" />
|
|
10
11
|
<change beforePath="$PROJECT_DIR$/src/obstruct/aot/unpack.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/obstruct/aot/unpack.py" afterDir="false" />
|
|
11
|
-
<change beforePath="$PROJECT_DIR$/src/obstruct/decorator.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/obstruct/decorator.py" afterDir="false" />
|
|
12
12
|
<change beforePath="$PROJECT_DIR$/src/obstruct/methods.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/obstruct/methods.py" afterDir="false" />
|
|
13
|
-
<change beforePath="$PROJECT_DIR$/
|
|
13
|
+
<change beforePath="$PROJECT_DIR$/test/test_speed.py" beforeDir="false" afterPath="$PROJECT_DIR$/test/test_speed.py" afterDir="false" />
|
|
14
14
|
</list>
|
|
15
15
|
<option name="SHOW_DIALOG" value="false" />
|
|
16
16
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
@@ -27,22 +27,22 @@
|
|
|
27
27
|
<component name="Git.Settings">
|
|
28
28
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
29
29
|
</component>
|
|
30
|
-
<component name="GitHubPullRequestSearchHistory"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
<component name="GitHubPullRequestSearchHistory">{
|
|
31
|
+
"lastFilter": {
|
|
32
|
+
"state": "OPEN",
|
|
33
|
+
"assignee": "LittleNightSong"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
36
|
-
<component name="GithubPullRequestsUISettings"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
}</component>
|
|
36
|
+
<component name="GithubPullRequestsUISettings">{
|
|
37
|
+
"selectedUrlAndAccountId": {
|
|
38
|
+
"url": "https://github.com/LittleNightSong/obstruct.git",
|
|
39
|
+
"accountId": "c74c50b1-2437-429b-b395-d763b566b8a2"
|
|
40
40
|
}
|
|
41
|
-
}
|
|
42
|
-
<component name="ProjectColorInfo"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
41
|
+
}</component>
|
|
42
|
+
<component name="ProjectColorInfo">{
|
|
43
|
+
"associatedIndex": 3,
|
|
44
|
+
"fromUser": false
|
|
45
|
+
}</component>
|
|
46
46
|
<component name="ProjectId" id="3HzAgHvw3X1T2NidzBlYQSHnZow" />
|
|
47
47
|
<component name="ProjectViewState">
|
|
48
48
|
<option name="hideEmptyMiddlePackages" value="true" />
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"Python.test.executor": "Run",
|
|
55
55
|
"Python.test_speed.executor": "Run",
|
|
56
56
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
57
|
+
"RunOnceActivity.git.unshallow": "true",
|
|
57
58
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
|
58
59
|
"codeWithMe.voiceChat.enabledByDefault": "false",
|
|
59
60
|
"git-widget-placeholder": "main",
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
}]]></component>
|
|
70
71
|
<component name="RunManager" selected="Python.test_speed">
|
|
71
72
|
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
|
72
|
-
<module name="
|
|
73
|
+
<module name="obstruct" />
|
|
73
74
|
<option name="ENV_FILES" value="" />
|
|
74
75
|
<option name="INTERPRETER_OPTIONS" value="" />
|
|
75
76
|
<option name="PARENT_ENVS" value="true" />
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
<method v="2" />
|
|
94
95
|
</configuration>
|
|
95
96
|
<configuration name="test_speed" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
|
96
|
-
<module name="
|
|
97
|
+
<module name="obstruct" />
|
|
97
98
|
<option name="ENV_FILES" value="" />
|
|
98
99
|
<option name="INTERPRETER_OPTIONS" value="" />
|
|
99
100
|
<option name="PARENT_ENVS" value="true" />
|
|
@@ -146,7 +147,31 @@
|
|
|
146
147
|
<option name="project" value="LOCAL" />
|
|
147
148
|
<updated>1786865208583</updated>
|
|
148
149
|
</task>
|
|
149
|
-
<
|
|
150
|
+
<task id="LOCAL-00002" summary="增强类型系统,撰写 README.md">
|
|
151
|
+
<option name="closed" value="true" />
|
|
152
|
+
<created>1786868730474</created>
|
|
153
|
+
<option name="number" value="00002" />
|
|
154
|
+
<option name="presentableId" value="LOCAL-00002" />
|
|
155
|
+
<option name="project" value="LOCAL" />
|
|
156
|
+
<updated>1786868730474</updated>
|
|
157
|
+
</task>
|
|
158
|
+
<task id="LOCAL-00003" summary="补充:基准测试脚本">
|
|
159
|
+
<option name="closed" value="true" />
|
|
160
|
+
<created>1786868746817</created>
|
|
161
|
+
<option name="number" value="00003" />
|
|
162
|
+
<option name="presentableId" value="LOCAL-00003" />
|
|
163
|
+
<option name="project" value="LOCAL" />
|
|
164
|
+
<updated>1786868746817</updated>
|
|
165
|
+
</task>
|
|
166
|
+
<task id="LOCAL-00004" summary="修正了一些命名">
|
|
167
|
+
<option name="closed" value="true" />
|
|
168
|
+
<created>1786870314349</created>
|
|
169
|
+
<option name="number" value="00004" />
|
|
170
|
+
<option name="presentableId" value="LOCAL-00004" />
|
|
171
|
+
<option name="project" value="LOCAL" />
|
|
172
|
+
<updated>1786870314349</updated>
|
|
173
|
+
</task>
|
|
174
|
+
<option name="localTasksCounter" value="5" />
|
|
150
175
|
<servers />
|
|
151
176
|
</component>
|
|
152
177
|
<component name="TypeScriptGeneratedFilesManager">
|
|
@@ -165,6 +190,9 @@
|
|
|
165
190
|
</component>
|
|
166
191
|
<component name="VcsManagerConfiguration">
|
|
167
192
|
<MESSAGE value="initial commit" />
|
|
168
|
-
<
|
|
193
|
+
<MESSAGE value="增强类型系统,撰写 README.md" />
|
|
194
|
+
<MESSAGE value="补充:基准测试脚本" />
|
|
195
|
+
<MESSAGE value="修正了一些命名" />
|
|
196
|
+
<option name="LAST_COMMIT_MESSAGE" value="修正了一些命名" />
|
|
169
197
|
</component>
|
|
170
198
|
</project>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: obstruct
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Author-email: LittleNightSong <LittleNightSongYO@outlook.com>
|
|
5
5
|
Classifier: Development Status :: 3 - Alpha
|
|
6
6
|
Classifier: Programming Language :: Python
|
|
@@ -15,27 +15,48 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
_来自已弃坑的 `Hydrogenlib`,重拾起当初的决心_
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
>[!TIP]
|
|
18
|
+
> [!TIP]
|
|
19
19
|
> 这个项目来自 [`HydrogenLib`](https://github.com/LittleNightSong/HydrogenLib)的 `Objective-Struct` 模块,脱离大项目的版本
|
|
20
20
|
>
|
|
21
21
|
> 同时,`HydrogenLib`中的 `Objective-Struct` 不再会更新
|
|
22
22
|
|
|
23
|
+
> [!TIP]
|
|
24
|
+
> 作者的主要开发精力放在了另一些项目上,也许很长时间这个项目都不会再更新
|
|
23
25
|
|
|
24
26
|
# OBStruct
|
|
27
|
+
|
|
25
28
|
_一个简单、纯粹、极速的二进制解析库_
|
|
26
29
|
|
|
27
30
|
## 亮点
|
|
31
|
+
|
|
32
|
+
- 纯 Python
|
|
28
33
|
- 基于类型注解的字段声明方式
|
|
29
34
|
- 支持 `struct` 标准库中的所有格式,并额外支持 array 类型
|
|
30
35
|
- 基于 AOT 的加速
|
|
31
36
|
- 完整的操作支持(打包、解包、流式处理)
|
|
32
37
|
|
|
38
|
+
> [!WARNING]
|
|
39
|
+
>使用 `array` 类型时,建议不要传入 list 类型或其它 Python 序列,
|
|
40
|
+
> 可能会带来极大的性能影响
|
|
41
|
+
|
|
42
|
+
## 基准测试
|
|
43
|
+
|
|
44
|
+
### 系统环境
|
|
45
|
+
|
|
46
|
+
- 12th Gen Intel® Core (TM) i7-12700H
|
|
47
|
+
|
|
48
|
+
在 12个字段,10000 次操作,8轮取平均值的条件下:
|
|
49
|
+
|
|
50
|
+
- 打包:269.639ns 一次操作
|
|
51
|
+
- 解包:582.185ns 一次操作
|
|
52
|
+
|
|
33
53
|
## 安装
|
|
54
|
+
|
|
34
55
|
```commandline
|
|
35
56
|
pip install obstruct
|
|
36
57
|
```
|
|
37
|
-
如果使用 `uv` 等包管理器,则按照工具的方法安装即可
|
|
38
58
|
|
|
59
|
+
如果使用 `uv` 等包管理器,则按照工具的方法安装即可
|
|
39
60
|
|
|
40
61
|
## 用法
|
|
41
62
|
|
|
@@ -45,6 +66,7 @@ pip install obstruct
|
|
|
45
66
|
import obstruct as obs
|
|
46
67
|
from dataclasses import dataclass
|
|
47
68
|
|
|
69
|
+
|
|
48
70
|
@obs.struct()
|
|
49
71
|
@dataclass()
|
|
50
72
|
class MyStruct(obs.Struct):
|
|
@@ -53,24 +75,25 @@ class MyStruct(obs.Struct):
|
|
|
53
75
|
c: obs.long
|
|
54
76
|
d: obs.unsigned_short
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
|
|
57
79
|
if __name__ == '__main__':
|
|
58
80
|
data = MyStruct(a=0, b=2.0, c=10000, d=14545)
|
|
59
|
-
|
|
81
|
+
|
|
60
82
|
packed_data = data.pack()
|
|
61
83
|
print(packed_data)
|
|
62
|
-
|
|
84
|
+
|
|
63
85
|
restored = MyStruct.unpack(packed_data)
|
|
64
86
|
print(restored)
|
|
65
|
-
|
|
87
|
+
|
|
66
88
|
print(data == restored)
|
|
67
89
|
|
|
68
90
|
```
|
|
69
91
|
|
|
70
|
-
在上述示例中,你可能发现 `data != restored
|
|
71
|
-
一般来说,我们不建议使用浮点数类型
|
|
92
|
+
在上述示例中,你可能发现 `data != restored`,这是浮点数的精度限制导致的。 因此,我们不建议使用浮点数类型,浮点数在二进制传输中也不常见。
|
|
72
93
|
|
|
73
94
|
## 计划
|
|
95
|
+
|
|
74
96
|
- 展平装饰器,目前的 API 较为繁琐,需要简化为唯一的 `Struct` 基类
|
|
75
97
|
- 继续优化性能
|
|
76
98
|
- 通过存根文件强制表明类型似乎变得不再有效了
|
|
99
|
+
- 支持 Tag 联合类型,对标 `msgspec`
|
|
@@ -1,27 +1,48 @@
|
|
|
1
1
|
_来自已弃坑的 `Hydrogenlib`,重拾起当初的决心_
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
>[!TIP]
|
|
4
|
+
> [!TIP]
|
|
5
5
|
> 这个项目来自 [`HydrogenLib`](https://github.com/LittleNightSong/HydrogenLib)的 `Objective-Struct` 模块,脱离大项目的版本
|
|
6
6
|
>
|
|
7
7
|
> 同时,`HydrogenLib`中的 `Objective-Struct` 不再会更新
|
|
8
8
|
|
|
9
|
+
> [!TIP]
|
|
10
|
+
> 作者的主要开发精力放在了另一些项目上,也许很长时间这个项目都不会再更新
|
|
9
11
|
|
|
10
12
|
# OBStruct
|
|
13
|
+
|
|
11
14
|
_一个简单、纯粹、极速的二进制解析库_
|
|
12
15
|
|
|
13
16
|
## 亮点
|
|
17
|
+
|
|
18
|
+
- 纯 Python
|
|
14
19
|
- 基于类型注解的字段声明方式
|
|
15
20
|
- 支持 `struct` 标准库中的所有格式,并额外支持 array 类型
|
|
16
21
|
- 基于 AOT 的加速
|
|
17
22
|
- 完整的操作支持(打包、解包、流式处理)
|
|
18
23
|
|
|
24
|
+
> [!WARNING]
|
|
25
|
+
>使用 `array` 类型时,建议不要传入 list 类型或其它 Python 序列,
|
|
26
|
+
> 可能会带来极大的性能影响
|
|
27
|
+
|
|
28
|
+
## 基准测试
|
|
29
|
+
|
|
30
|
+
### 系统环境
|
|
31
|
+
|
|
32
|
+
- 12th Gen Intel® Core (TM) i7-12700H
|
|
33
|
+
|
|
34
|
+
在 12个字段,10000 次操作,8轮取平均值的条件下:
|
|
35
|
+
|
|
36
|
+
- 打包:269.639ns 一次操作
|
|
37
|
+
- 解包:582.185ns 一次操作
|
|
38
|
+
|
|
19
39
|
## 安装
|
|
40
|
+
|
|
20
41
|
```commandline
|
|
21
42
|
pip install obstruct
|
|
22
43
|
```
|
|
23
|
-
如果使用 `uv` 等包管理器,则按照工具的方法安装即可
|
|
24
44
|
|
|
45
|
+
如果使用 `uv` 等包管理器,则按照工具的方法安装即可
|
|
25
46
|
|
|
26
47
|
## 用法
|
|
27
48
|
|
|
@@ -31,6 +52,7 @@ pip install obstruct
|
|
|
31
52
|
import obstruct as obs
|
|
32
53
|
from dataclasses import dataclass
|
|
33
54
|
|
|
55
|
+
|
|
34
56
|
@obs.struct()
|
|
35
57
|
@dataclass()
|
|
36
58
|
class MyStruct(obs.Struct):
|
|
@@ -39,24 +61,25 @@ class MyStruct(obs.Struct):
|
|
|
39
61
|
c: obs.long
|
|
40
62
|
d: obs.unsigned_short
|
|
41
63
|
|
|
42
|
-
|
|
64
|
+
|
|
43
65
|
if __name__ == '__main__':
|
|
44
66
|
data = MyStruct(a=0, b=2.0, c=10000, d=14545)
|
|
45
|
-
|
|
67
|
+
|
|
46
68
|
packed_data = data.pack()
|
|
47
69
|
print(packed_data)
|
|
48
|
-
|
|
70
|
+
|
|
49
71
|
restored = MyStruct.unpack(packed_data)
|
|
50
72
|
print(restored)
|
|
51
|
-
|
|
73
|
+
|
|
52
74
|
print(data == restored)
|
|
53
75
|
|
|
54
76
|
```
|
|
55
77
|
|
|
56
|
-
在上述示例中,你可能发现 `data != restored
|
|
57
|
-
一般来说,我们不建议使用浮点数类型
|
|
78
|
+
在上述示例中,你可能发现 `data != restored`,这是浮点数的精度限制导致的。 因此,我们不建议使用浮点数类型,浮点数在二进制传输中也不常见。
|
|
58
79
|
|
|
59
80
|
## 计划
|
|
81
|
+
|
|
60
82
|
- 展平装饰器,目前的 API 较为繁琐,需要简化为唯一的 `Struct` 基类
|
|
61
83
|
- 继续优化性能
|
|
62
84
|
- 通过存根文件强制表明类型似乎变得不再有效了
|
|
85
|
+
- 支持 Tag 联合类型,对标 `msgspec`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.2.4"
|
|
@@ -31,18 +31,18 @@ class _objective_node_operator:
|
|
|
31
31
|
self._node.body.append(tree)
|
|
32
32
|
return tree
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
def insert_code(self, index, code: str):
|
|
35
|
+
tree = ast.parse(code)
|
|
36
|
+
if isinstance(tree, ast.Module):
|
|
37
|
+
for node in reversed(tree.body):
|
|
38
|
+
self._node.body.insert(index, node)
|
|
39
|
+
return tree
|
|
40
|
+
elif isinstance(tree, ast.Expr):
|
|
41
|
+
self._node.body.insert(index, tree.value)
|
|
42
|
+
return tree.value
|
|
43
|
+
else:
|
|
44
|
+
self._node.body.insert(index, tree)
|
|
45
|
+
return tree
|
|
46
46
|
|
|
47
47
|
def compile(self):
|
|
48
48
|
module = ast.Module([self._node])
|
|
@@ -54,6 +54,7 @@ class _objective_node_operator:
|
|
|
54
54
|
def exec(self, globals=None, locals=None, closure=None):
|
|
55
55
|
globals = globals or {}
|
|
56
56
|
exec(self.compile(), globals, locals, closure=closure)
|
|
57
|
+
# print(repr(globals))
|
|
57
58
|
return globals[self._node.name]
|
|
58
59
|
|
|
59
60
|
def generate_code(self):
|
|
@@ -137,5 +138,9 @@ def get_array_ctype(tp) -> str | None | Any:
|
|
|
137
138
|
return get_ctype(get_array_spec(tp)[0])
|
|
138
139
|
|
|
139
140
|
|
|
141
|
+
def get_array_length(tp):
|
|
142
|
+
return get_array_spec(tp)[1]
|
|
143
|
+
|
|
144
|
+
|
|
140
145
|
def to_tuple_expression(names, prefix='self.'):
|
|
141
146
|
return ', '.join(f'{prefix}{name}' for name in names)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import array
|
|
4
|
+
import struct
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from .base import unique_name_generator, is_struct, is_array, get_array_ctype, to_tuple_expression, function_def, \
|
|
8
|
+
get_array_length
|
|
9
|
+
from ..methods import get_ctype
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
from .. import struct_type
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_pack_methods(cls: struct_type.Struct):
|
|
16
|
+
"""
|
|
17
|
+
Generate `pack` and `pack_into` method
|
|
18
|
+
"""
|
|
19
|
+
with (
|
|
20
|
+
function_def('pack(self)') as pack,
|
|
21
|
+
function_def('pack_into(self, buffer, offset=0)') as pack_into,
|
|
22
|
+
):
|
|
23
|
+
ung = unique_name_generator()
|
|
24
|
+
flat_fields = []
|
|
25
|
+
array_fields = []
|
|
26
|
+
|
|
27
|
+
pack_into.add_code('buffer = memoryview(buffer)')
|
|
28
|
+
|
|
29
|
+
def add_code(code):
|
|
30
|
+
pack.add_code(code)
|
|
31
|
+
pack_into.add_code(code)
|
|
32
|
+
|
|
33
|
+
def walk(fields, path, parent_name=None, offset=0):
|
|
34
|
+
# parent name 标识了这个字段的父字段被赋值给了哪个临时变量,这样可以复用已有的临时变量,减少结构体属性访问次数
|
|
35
|
+
name = path[-1] # 这里的 name 是默认的 self,如果当前处理的是内嵌的结构体,那么 name 会被替换成临时变量
|
|
36
|
+
|
|
37
|
+
if len(path) > 1: # 当前处理的是内嵌结构体
|
|
38
|
+
name = next(ung) # 生成一个临时变量缓存其值
|
|
39
|
+
add_code(f'{name} = {parent_name}.{path[-1]}')
|
|
40
|
+
|
|
41
|
+
for field in fields: # 遍历结构体的所有字段
|
|
42
|
+
if is_struct(field.type): # 又是一个内嵌的结构体字段
|
|
43
|
+
walk(field.type.__cfields__, path + (field.name,), name, offset) # 递归
|
|
44
|
+
|
|
45
|
+
elif is_array(field.type): # 一个数组类型
|
|
46
|
+
dtype = get_array_ctype(field.type)
|
|
47
|
+
nbytes = struct.calcsize(dtype) * get_array_length(field.type)
|
|
48
|
+
|
|
49
|
+
_ = next(ung)
|
|
50
|
+
add_code(f'{_} = array({dtype!r}, {name}.{field.name})')
|
|
51
|
+
# flat_fields.append(f'{_}.tobytes()')
|
|
52
|
+
array_fields.append((_, offset, nbytes))
|
|
53
|
+
|
|
54
|
+
else:
|
|
55
|
+
flat_fields.append(f'{name}.{field.name}') # 普通的字段,添加到传递目标列表中
|
|
56
|
+
|
|
57
|
+
offset += struct.calcsize(get_ctype(field.type))
|
|
58
|
+
|
|
59
|
+
walk(cls.__cfields__, ('self',)) # 启动递归函数
|
|
60
|
+
# names = [field.name for field in fields]
|
|
61
|
+
|
|
62
|
+
expression = to_tuple_expression(flat_fields, prefix='')
|
|
63
|
+
|
|
64
|
+
if array_fields: # 如果有数组字段,我们需要手动操作缓冲区
|
|
65
|
+
pack.add_code(f"buffer = bytearray({cls.__cstruct__.size})") # 初始化缓冲区
|
|
66
|
+
for array_name, offset, size in array_fields:
|
|
67
|
+
add_code(f"buffer[{offset}:{offset + size}] = memoryview({array_name}).cast('B')")
|
|
68
|
+
|
|
69
|
+
pack.add_code(f"_pack_into(buffer, 0, {expression})") # 返回打包的结果,将所有字段参数展开传递
|
|
70
|
+
pack.add_return("buffer")
|
|
71
|
+
else:
|
|
72
|
+
pack.add_return(f"_pack({expression})")
|
|
73
|
+
|
|
74
|
+
pack_into.add_code(f"_pack_into(buffer, offset, {expression})")
|
|
75
|
+
print(pack.generate_code()) # Debug
|
|
76
|
+
print(pack_into.generate_code()) # Debug
|
|
77
|
+
return (
|
|
78
|
+
pack.exec({'_pack': cls.__cstruct__.pack, '_pack_into': cls.__cstruct__.pack_into, 'array': array.array}),
|
|
79
|
+
pack_into.exec({'_pack_into': cls.__cstruct__.pack_into, 'array': array.array})
|
|
80
|
+
)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import array
|
|
4
|
+
import struct
|
|
4
5
|
import typing
|
|
5
6
|
|
|
6
|
-
from .base import unique_name_generator, is_struct, is_array, get_array_ctype, function_def, for_stmt
|
|
7
|
+
from .base import unique_name_generator, is_struct, is_array, get_array_ctype, function_def, for_stmt, get_array_length
|
|
8
|
+
from ..methods import get_ctype
|
|
7
9
|
|
|
8
10
|
if typing.TYPE_CHECKING:
|
|
9
11
|
from .. import struct_type
|
|
@@ -23,6 +25,8 @@ def create_unpack_methods(cls: type[struct_type.Struct]):
|
|
|
23
25
|
iter_unpack.add_decorator('classmethod')
|
|
24
26
|
iter_update.add_decorator('classmethod')
|
|
25
27
|
|
|
28
|
+
# unpack.add_code('buffer = memoryview(buffer)')
|
|
29
|
+
|
|
26
30
|
ung = unique_name_generator()
|
|
27
31
|
|
|
28
32
|
def update_add_code(code):
|
|
@@ -35,23 +39,25 @@ def create_unpack_methods(cls: type[struct_type.Struct]):
|
|
|
35
39
|
unpack_from.add_code(code)
|
|
36
40
|
iter_unpack_inner.add_code(code)
|
|
37
41
|
|
|
38
|
-
post_processors = [] # 后处理代码列表
|
|
39
42
|
assigns = [] # 赋值目标列表
|
|
40
43
|
inner_classes = {} # 内嵌的结构体的类型映射(所有内嵌结构体的类型在生成代码时都会被代替为无意义的名称)
|
|
41
44
|
|
|
42
45
|
iter_update.add_code("self = cls() if obj is None else obj")
|
|
43
46
|
|
|
44
|
-
iter_unpack_inner = for_stmt("values in
|
|
45
|
-
iter_update_inner = for_stmt("values in
|
|
47
|
+
iter_unpack_inner = for_stmt("values in _iter_unpack(buffer)")
|
|
48
|
+
iter_update_inner = for_stmt("values in _iter_unpack(buffer)")
|
|
46
49
|
|
|
47
50
|
iter_unpack.add_node(iter_unpack_inner.node)
|
|
48
51
|
iter_update.add_node(iter_update_inner.node)
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
array_fields = []
|
|
54
|
+
|
|
55
|
+
def walk(fields, path, parent_name=None, current_class_name='cls', offset=0):
|
|
51
56
|
name = 'self'
|
|
52
57
|
|
|
58
|
+
|
|
53
59
|
if len(path) > 1: # 在处理内嵌的结构体
|
|
54
|
-
name =
|
|
60
|
+
name = next(ung) # 临时变量的名称
|
|
55
61
|
update_add_code(f"{name} = {parent_name}.{path[-1]}")
|
|
56
62
|
|
|
57
63
|
unpack_add_code(f"{parent_name}.{path[-1]} = {name} = __new__({current_class_name})") # 组合赋值
|
|
@@ -60,44 +66,54 @@ def create_unpack_methods(cls: type[struct_type.Struct]):
|
|
|
60
66
|
# update 在这里不需要创建新对象
|
|
61
67
|
|
|
62
68
|
for field in fields: # 遍历字段
|
|
63
|
-
|
|
69
|
+
field_xpath = f'{name}.{field.name}'
|
|
64
70
|
|
|
65
71
|
if is_struct(field.type): # 是个内嵌的结构体
|
|
66
72
|
if field.type not in inner_classes: # 如果这个内嵌的结构体类型不在已知的结构体中(没有分配类名称)
|
|
67
|
-
type_name =
|
|
73
|
+
type_name = next(ung) # 生成一个唯一的类名
|
|
68
74
|
inner_classes[field.type] = type_name # 添加
|
|
69
75
|
else:
|
|
70
76
|
type_name = inner_classes[field.type] # 如果在,那么提取
|
|
71
77
|
|
|
72
|
-
walk(field.type.__cfields__, path + (field.name,), name, type_name)
|
|
78
|
+
walk(field.type.__cfields__, path + (field.name,), name, type_name, offset)
|
|
79
|
+
|
|
73
80
|
elif is_array(field.type):
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
f'{field_ref} = _'
|
|
79
|
-
)
|
|
80
|
-
assigns.append(field_ref)
|
|
81
|
+
dtype = get_array_ctype(field.type)
|
|
82
|
+
nbytes = get_array_length(field.type) * struct.calcsize(dtype)
|
|
83
|
+
array_fields.append((field_xpath, dtype, offset, nbytes))
|
|
84
|
+
|
|
81
85
|
else:
|
|
82
|
-
assigns.append(
|
|
86
|
+
assigns.append(field_xpath)
|
|
87
|
+
|
|
88
|
+
offset += struct.calcsize(get_ctype(field.type))
|
|
83
89
|
|
|
84
90
|
walk(cls.__cfields__, ('A',))
|
|
85
91
|
|
|
86
|
-
assign_code = f"{', '.join(assigns)} =
|
|
92
|
+
assign_code = f"{', '.join(assigns)} = _unpack(buffer)" # 生成普通的 unpack, update 用的赋值语句
|
|
93
|
+
# {assigns} = _unpack(buffer)
|
|
87
94
|
unpack.add_code(assign_code)
|
|
88
95
|
update.add_code(assign_code)
|
|
89
96
|
|
|
90
|
-
assign_code = assign_code.replace("
|
|
97
|
+
assign_code = assign_code.replace("_unpack(buffer)", "values") # 生成 iter_xxx 的赋值语句
|
|
98
|
+
# {assigns} = values
|
|
91
99
|
iter_unpack_inner.add_code(assign_code)
|
|
92
100
|
iter_update_inner.add_code(assign_code)
|
|
93
101
|
|
|
94
|
-
assign_code = assign_code.replace("values", "
|
|
102
|
+
assign_code = assign_code.replace("values", "_unpack_from(buffer, offset)") # 生成 xxx_from 的赋值语句
|
|
103
|
+
# {assigns} = _unpack_from(buffer, offset)
|
|
95
104
|
unpack_from.add_code(assign_code)
|
|
96
105
|
update_from.add_code(assign_code)
|
|
97
106
|
|
|
98
|
-
for
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
for field_xpath, dtype, offset, nbytes in array_fields:
|
|
108
|
+
codes = [
|
|
109
|
+
f'{field_xpath} = _ = array({dtype!r})',
|
|
110
|
+
f'_.frombytes(buffer[{offset}:{offset+nbytes}])',
|
|
111
|
+
]
|
|
112
|
+
for co in codes:
|
|
113
|
+
unpack.add_code(co)
|
|
114
|
+
|
|
115
|
+
if array_fields:
|
|
116
|
+
unpack.insert_code(0, 'buffer = memoryview(buffer)')
|
|
101
117
|
|
|
102
118
|
unpack.add_return('self')
|
|
103
119
|
update.add_return('self')
|
|
@@ -107,7 +123,7 @@ def create_unpack_methods(cls: type[struct_type.Struct]):
|
|
|
107
123
|
iter_unpack_inner.add_code('yield self') # iter 方法是 yield,而非 return
|
|
108
124
|
iter_update_inner.add_code('yield self')
|
|
109
125
|
|
|
110
|
-
|
|
126
|
+
print(unpack.generate_code())
|
|
111
127
|
# print(update.generate_code())
|
|
112
128
|
# print(unpack_from.generate_code())
|
|
113
129
|
# print(update_from.generate_code())
|
|
@@ -120,9 +136,9 @@ def create_unpack_methods(cls: type[struct_type.Struct]):
|
|
|
120
136
|
|
|
121
137
|
globals.update(dict(
|
|
122
138
|
__new__=object.__new__,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
139
|
+
_unpack=cls.__cstruct__.unpack,
|
|
140
|
+
_unpack_from=cls.__cstruct__.unpack_from,
|
|
141
|
+
_iter_unpack=cls.__cstruct__.iter_unpack,
|
|
126
142
|
array=array.array
|
|
127
143
|
))
|
|
128
144
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import time
|
|
3
|
+
from array import array
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import obstruct as obs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@obs.struct()
|
|
10
|
+
@dataclasses.dataclass(slots=True)
|
|
11
|
+
class MyStruct(obs.Struct):
|
|
12
|
+
a: int
|
|
13
|
+
b: int
|
|
14
|
+
c: float
|
|
15
|
+
d: int
|
|
16
|
+
e: obs.short
|
|
17
|
+
f: obs.unsigned_short
|
|
18
|
+
g: obs.long
|
|
19
|
+
h: obs.unsigned_long
|
|
20
|
+
i: obs.unsigned_long
|
|
21
|
+
j: obs.char
|
|
22
|
+
k: obs.unsigned_char
|
|
23
|
+
l: obs.long_long
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@obs.struct()
|
|
27
|
+
@dataclasses.dataclass(slots=True)
|
|
28
|
+
class AnotherStruct(obs.Struct):
|
|
29
|
+
x: MyStruct
|
|
30
|
+
y: Annotated[array[int], 100]
|
|
31
|
+
z: MyStruct
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
data = MyStruct(
|
|
35
|
+
1,2,1.0,4,5,6,7,8,9,b'8',1,2
|
|
36
|
+
)
|
|
37
|
+
packed_data = data.pack()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
complex_data = AnotherStruct(data, array('i', [100]*100), data)
|
|
41
|
+
packed_complex_data = complex_data.pack()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_timeit(name, code):
|
|
45
|
+
import timeit
|
|
46
|
+
|
|
47
|
+
print("Test: ", name)
|
|
48
|
+
res = timeit.repeat(
|
|
49
|
+
code,
|
|
50
|
+
number=NUMBER,
|
|
51
|
+
repeat=REPEAT,
|
|
52
|
+
timer=time.perf_counter_ns,
|
|
53
|
+
globals=globals()
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
print(len(res))
|
|
57
|
+
|
|
58
|
+
for i in range(SORT):
|
|
59
|
+
res.remove(max(res))
|
|
60
|
+
res.remove(min(res))
|
|
61
|
+
|
|
62
|
+
print("Results Length:", len(res))
|
|
63
|
+
|
|
64
|
+
print(f'{NUMBER} steps finished in {sum(res)/(len(res)):.3f} ns')
|
|
65
|
+
print(f'Average: {sum(res)/(len(res))/NUMBER:.3f} ns per step', '\n\n')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
print("\n\nMyStruct:", MyStruct.__cstruct__.size)
|
|
69
|
+
print("AnotherStruct:", AnotherStruct.__cstruct__.size, '\n\n')
|
|
70
|
+
|
|
71
|
+
NUMBER = 10000
|
|
72
|
+
REPEAT = 128
|
|
73
|
+
SORT = 32
|
|
74
|
+
|
|
75
|
+
test_timeit(
|
|
76
|
+
name='Pack',
|
|
77
|
+
code="""data.pack()""",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
test_timeit(
|
|
81
|
+
name='Unpack',
|
|
82
|
+
code="""MyStruct.unpack(packed_data)""",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
test_timeit(
|
|
88
|
+
name='Complex Pack',
|
|
89
|
+
code="""complex_data.pack()""",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
test_timeit(
|
|
93
|
+
name='Complex Unpack',
|
|
94
|
+
code="""AnotherStruct.unpack(packed_complex_data)""",
|
|
95
|
+
)
|
obstruct-0.2.2/.idea/modules.xml
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="ProjectModuleManager">
|
|
4
|
-
<modules>
|
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/Hydrogenlib-Objective-Struct.iml" filepath="$PROJECT_DIR$/.idea/Hydrogenlib-Objective-Struct.iml" />
|
|
6
|
-
</modules>
|
|
7
|
-
</component>
|
|
8
|
-
</project>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
version = "0.2.2"
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import array
|
|
4
|
-
import typing
|
|
5
|
-
|
|
6
|
-
from .base import unique_name_generator, is_struct, is_array, get_array_ctype, to_tuple_expression, function_def
|
|
7
|
-
|
|
8
|
-
if typing.TYPE_CHECKING:
|
|
9
|
-
from .. import struct_type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def create_pack_methods(cls: struct_type.Struct):
|
|
13
|
-
"""
|
|
14
|
-
Generate `pack` and `pack_into` method
|
|
15
|
-
"""
|
|
16
|
-
with (
|
|
17
|
-
function_def('pack(self)') as pack,
|
|
18
|
-
function_def('pack_into(self, buffer, offset=0)') as pack_into,
|
|
19
|
-
):
|
|
20
|
-
ung = unique_name_generator()
|
|
21
|
-
flat_fields = []
|
|
22
|
-
|
|
23
|
-
def add_code(code):
|
|
24
|
-
pack.add_code(code)
|
|
25
|
-
pack_into.add_code(code)
|
|
26
|
-
|
|
27
|
-
def walk(fields, path, parent_name=None):
|
|
28
|
-
# parent name 标识了这个字段的父字段被赋值给了哪个临时变量,这样可以复用已有的临时变量,减少结构体属性访问次数
|
|
29
|
-
name = 'self' # 这里的 name 是默认的 self,如果当前处理的是内嵌的结构体,那么 name 会被替换成临时变量
|
|
30
|
-
|
|
31
|
-
if len(path) > 1: # 当前处理的是内嵌结构体
|
|
32
|
-
name = ung.next() # 生成一个临时变量缓存其值
|
|
33
|
-
add_code(f'{name} = {parent_name}.{path[-1]}')
|
|
34
|
-
|
|
35
|
-
for field in fields: # 遍历结构体的所有字段
|
|
36
|
-
if is_struct(field.type): # 又是一个内嵌的结构体字段
|
|
37
|
-
walk(field.type.__cfields__, path + (field.name,), name) # 递归
|
|
38
|
-
elif is_array(field.type): # 一个数组类型
|
|
39
|
-
d_ctype = get_array_ctype(field.type)
|
|
40
|
-
|
|
41
|
-
_ = ung.next()
|
|
42
|
-
add_code(f'{_} = array({d_ctype!r}, {name}.{field.name})')
|
|
43
|
-
flat_fields.append(f'{_}.tobytes()')
|
|
44
|
-
|
|
45
|
-
else:
|
|
46
|
-
flat_fields.append(f'{name}.{field.name}') # 普通的字段,添加到传递目标列表中
|
|
47
|
-
|
|
48
|
-
walk(cls.__cfields__, ('self',)) # 启动递归函数
|
|
49
|
-
# names = [field.name for field in fields]
|
|
50
|
-
|
|
51
|
-
expression = to_tuple_expression(flat_fields, prefix='')
|
|
52
|
-
pack.add_return(f"xpack({expression})") # 返回打包的结果,将所有字段参数展开传递
|
|
53
|
-
pack_into.add_code(f"xpack_into(buffer, offset, {expression})")
|
|
54
|
-
# print(pack.generate_code()) # Debug
|
|
55
|
-
return (
|
|
56
|
-
pack.exec({'xpack': cls.__cstruct__.pack, 'array': array.array}),
|
|
57
|
-
pack_into.exec({'xpack_into': cls.__cstruct__.pack_into, 'array': array.array})
|
|
58
|
-
)
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import time
|
|
3
|
-
|
|
4
|
-
import obstruct as obs
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@obs.struct()
|
|
8
|
-
@dataclasses.dataclass(slots=True)
|
|
9
|
-
class MyStruct(obs.Struct):
|
|
10
|
-
a: int
|
|
11
|
-
b: int
|
|
12
|
-
c: float
|
|
13
|
-
d: int
|
|
14
|
-
e: obs.short
|
|
15
|
-
f: obs.unsigned_short
|
|
16
|
-
g: obs.long
|
|
17
|
-
h: obs.unsigned_long
|
|
18
|
-
i: obs.unsigned_long
|
|
19
|
-
j: obs.char
|
|
20
|
-
k: obs.unsigned_char
|
|
21
|
-
l: obs.long_long
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
data = MyStruct(
|
|
26
|
-
1,2,1.0,4,5,6,7,8,9,b'8',1,2
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
packed_data = data.pack()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_timeit(name, code):
|
|
34
|
-
import timeit
|
|
35
|
-
|
|
36
|
-
print("Test: ", name)
|
|
37
|
-
res = timeit.timeit(
|
|
38
|
-
code,
|
|
39
|
-
number=NUMBER,
|
|
40
|
-
timer=time.perf_counter_ns,
|
|
41
|
-
globals=globals()
|
|
42
|
-
)
|
|
43
|
-
print(f'{NUMBER} steps finished in {res:.3f} ns')
|
|
44
|
-
print(f'Average: {res/NUMBER:.3f} ns per step', '\n\n')
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
NUMBER = 10000
|
|
48
|
-
|
|
49
|
-
test_timeit(
|
|
50
|
-
name='Pack',
|
|
51
|
-
code="""data.pack()""",
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
test_timeit(
|
|
55
|
-
name='Unpack',
|
|
56
|
-
code="""MyStruct.unpack(packed_data)""",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|