pycrefine 1.1.1__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.
- pycrefine-1.1.1/LICENSE +21 -0
- pycrefine-1.1.1/PKG-INFO +123 -0
- pycrefine-1.1.1/README.md +91 -0
- pycrefine-1.1.1/pyproject.toml +79 -0
- pycrefine-1.1.1/setup.cfg +4 -0
- pycrefine-1.1.1/src/pycrefine/__init__.py +65 -0
- pycrefine-1.1.1/src/pycrefine/__main__.py +3 -0
- pycrefine-1.1.1/src/pycrefine/_constants.py +136 -0
- pycrefine-1.1.1/src/pycrefine/cli.py +53 -0
- pycrefine-1.1.1/src/pycrefine/codeobj.py +26 -0
- pycrefine-1.1.1/src/pycrefine/decompiler/__init__.py +0 -0
- pycrefine-1.1.1/src/pycrefine/decompiler/base.py +258 -0
- pycrefine-1.1.1/src/pycrefine/decompiler/generic.py +5942 -0
- pycrefine-1.1.1/src/pycrefine/decompiler/py311.py +117 -0
- pycrefine-1.1.1/src/pycrefine/decompiler/py314.py +219 -0
- pycrefine-1.1.1/src/pycrefine/decompiler/py39.py +1269 -0
- pycrefine-1.1.1/src/pycrefine/expr.py +91 -0
- pycrefine-1.1.1/src/pycrefine/instructions.py +393 -0
- pycrefine-1.1.1/src/pycrefine/loader.py +342 -0
- pycrefine-1.1.1/src/pycrefine/opcodes/__init__.py +0 -0
- pycrefine-1.1.1/src/pycrefine/opcodes/py39.py +135 -0
- pycrefine-1.1.1/src/pycrefine/postprocess.py +494 -0
- pycrefine-1.1.1/src/pycrefine/versions.py +41 -0
- pycrefine-1.1.1/src/pycrefine.egg-info/PKG-INFO +123 -0
- pycrefine-1.1.1/src/pycrefine.egg-info/SOURCES.txt +43 -0
- pycrefine-1.1.1/src/pycrefine.egg-info/dependency_links.txt +1 -0
- pycrefine-1.1.1/src/pycrefine.egg-info/entry_points.txt +2 -0
- pycrefine-1.1.1/src/pycrefine.egg-info/requires.txt +8 -0
- pycrefine-1.1.1/src/pycrefine.egg-info/top_level.txt +1 -0
- pycrefine-1.1.1/tests/test_basic.py +1143 -0
- pycrefine-1.1.1/tests/test_control_flow.py +1195 -0
- pycrefine-1.1.1/tests/test_correctness.py +285 -0
- pycrefine-1.1.1/tests/test_exceptions.py +1212 -0
- pycrefine-1.1.1/tests/test_fallback.py +33 -0
- pycrefine-1.1.1/tests/test_foreign_pyc_fixtures.py +43 -0
- pycrefine-1.1.1/tests/test_helpers.py +475 -0
- pycrefine-1.1.1/tests/test_instruction_regressions.py +116 -0
- pycrefine-1.1.1/tests/test_integration.py +799 -0
- pycrefine-1.1.1/tests/test_internals.py +840 -0
- pycrefine-1.1.1/tests/test_lambda_decompilation.py +67 -0
- pycrefine-1.1.1/tests/test_lint_ratchet.py +186 -0
- pycrefine-1.1.1/tests/test_metrics.py +374 -0
- pycrefine-1.1.1/tests/test_strings.py +96 -0
- pycrefine-1.1.1/tests/test_subroutines.py +439 -0
- pycrefine-1.1.1/tests/test_version_bump_check.py +55 -0
pycrefine-1.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saheb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pycrefine-1.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycrefine
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: A Python .pyc decompiler for educational purposes
|
|
5
|
+
Author: Saheb
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sahebbiswas/pycrefine
|
|
8
|
+
Project-URL: Issues, https://github.com/sahebbiswas/pycrefine/issues
|
|
9
|
+
Keywords: decompiler,bytecode,pyc,cpython,reverse-engineering
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Disassemblers
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest; extra == "test"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
Requires-Dist: pylint==4.1.3; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy==2.4.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# pycrefine
|
|
34
|
+
|
|
35
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py39.yml)
|
|
36
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py312.yml)
|
|
37
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py314.yml)
|
|
38
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/lint.yml)
|
|
39
|
+
|
|
40
|
+
pycrefine turns compiled Python bytecode (`.pyc` files) back into readable Python source.
|
|
41
|
+
It reads `.pyc` files written by CPython 3.9 and later, has no third-party dependencies,
|
|
42
|
+
and works both as a command and as a library.
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
$ pycrefine __pycache__/report.cpython-312.pyc
|
|
46
|
+
def summarize(rows, limit=10):
|
|
47
|
+
totals = {}
|
|
48
|
+
for name, amount in rows:
|
|
49
|
+
totals[name] = (totals.get(name, 0) + amount)
|
|
50
|
+
return sorted(totals.items(), key=(lambda kv: -kv[1]))[:limit]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Supported Python versions
|
|
54
|
+
|
|
55
|
+
| Bytecode written by | Status | Run pycrefine under |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Python 3.9 | supported, tested in CI | any Python 3.9 or later |
|
|
58
|
+
| Python 3.12, 3.14 | supported, tested in CI | the same minor version (3.12 for 3.12 bytecode) |
|
|
59
|
+
| Python 3.13 | works, not in CI | Python 3.13 |
|
|
60
|
+
| Python 3.10, 3.11 | partial: loops and exception handling often come out wrong | the same minor version |
|
|
61
|
+
| Python 3.8 and older | not supported | |
|
|
62
|
+
|
|
63
|
+
Bytecode from 3.10 onwards can only be decompiled by the Python version that wrote it
|
|
64
|
+
([#94](https://github.com/sahebbiswas/pycrefine/issues/94)). pycrefine tells you which
|
|
65
|
+
version to use, and [Getting started](https://github.com/sahebbiswas/pycrefine/blob/main/docs/getting-started.md#run-under-the-python-that-wrote-the-file)
|
|
66
|
+
shows how to run it under another Python in one command. Some constructs (`match`,
|
|
67
|
+
`async`/`await`, `yield from`) are not reconstructed yet; see
|
|
68
|
+
[Limitations](https://github.com/sahebbiswas/pycrefine/blob/main/docs/limitations.md).
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -m pip install pycrefine
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Until the first release reaches PyPI, install from GitHub instead:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pip install "git+https://github.com/sahebbiswas/pycrefine"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or run it from a checkout without installing: `python pycrefine.py file.pyc`. Install
|
|
83
|
+
problems (`command not found`, `externally-managed-environment`, …) are covered in
|
|
84
|
+
[Troubleshooting](https://github.com/sahebbiswas/pycrefine/blob/main/docs/troubleshooting.md#installation-and-setup).
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pycrefine module.cpython-312.pyc # print the source
|
|
90
|
+
pycrefine module.cpython-312.pyc -o module.py # save it to a file
|
|
91
|
+
python -m pycrefine module.cpython-312.pyc # same, if the command is not on PATH
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
From Python:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import pycrefine
|
|
98
|
+
|
|
99
|
+
source = pycrefine.get_decompiler("module.cpython-312.pyc").decompile()
|
|
100
|
+
print(source)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Documentation
|
|
104
|
+
|
|
105
|
+
| Guide | Read it to |
|
|
106
|
+
|---|---|
|
|
107
|
+
| [Getting started](https://github.com/sahebbiswas/pycrefine/blob/main/docs/getting-started.md) | install, decompile a first file, and run under the right Python version |
|
|
108
|
+
| [Use cases](https://github.com/sahebbiswas/pycrefine/blob/main/docs/use-cases.md) | recover a project from `__pycache__`, batch-decompile a tree, decompile PyInstaller or in-memory code, check the output |
|
|
109
|
+
| [Command line and Python API](https://github.com/sahebbiswas/pycrefine/blob/main/docs/usage.md) | look up every option, exit status and API call |
|
|
110
|
+
| [Troubleshooting](https://github.com/sahebbiswas/pycrefine/blob/main/docs/troubleshooting.md) | fix setup problems and understand each error message |
|
|
111
|
+
| [Limitations](https://github.com/sahebbiswas/pycrefine/blob/main/docs/limitations.md) | see what is not reconstructed yet, and how to tell |
|
|
112
|
+
| [Architecture](https://github.com/sahebbiswas/pycrefine/blob/main/docs/architecture.md) | find your way around the code |
|
|
113
|
+
| [Contributing](https://github.com/sahebbiswas/pycrefine/blob/main/CONTRIBUTING.md) | set up a development environment, run the checks, version and release |
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
Bug reports with the `.pyc` file (or the source it came from) and the Python version that
|
|
118
|
+
compiled it are the most useful contribution. See [CONTRIBUTING.md](https://github.com/sahebbiswas/pycrefine/blob/main/CONTRIBUTING.md) for
|
|
119
|
+
the development setup, the checks every pull request runs, and the versioning policy.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
[MIT](https://github.com/sahebbiswas/pycrefine/blob/main/LICENSE)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# pycrefine
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py39.yml)
|
|
4
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py312.yml)
|
|
5
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py314.yml)
|
|
6
|
+
[](https://github.com/sahebbiswas/pycrefine/actions/workflows/lint.yml)
|
|
7
|
+
|
|
8
|
+
pycrefine turns compiled Python bytecode (`.pyc` files) back into readable Python source.
|
|
9
|
+
It reads `.pyc` files written by CPython 3.9 and later, has no third-party dependencies,
|
|
10
|
+
and works both as a command and as a library.
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
$ pycrefine __pycache__/report.cpython-312.pyc
|
|
14
|
+
def summarize(rows, limit=10):
|
|
15
|
+
totals = {}
|
|
16
|
+
for name, amount in rows:
|
|
17
|
+
totals[name] = (totals.get(name, 0) + amount)
|
|
18
|
+
return sorted(totals.items(), key=(lambda kv: -kv[1]))[:limit]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Supported Python versions
|
|
22
|
+
|
|
23
|
+
| Bytecode written by | Status | Run pycrefine under |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Python 3.9 | supported, tested in CI | any Python 3.9 or later |
|
|
26
|
+
| Python 3.12, 3.14 | supported, tested in CI | the same minor version (3.12 for 3.12 bytecode) |
|
|
27
|
+
| Python 3.13 | works, not in CI | Python 3.13 |
|
|
28
|
+
| Python 3.10, 3.11 | partial: loops and exception handling often come out wrong | the same minor version |
|
|
29
|
+
| Python 3.8 and older | not supported | |
|
|
30
|
+
|
|
31
|
+
Bytecode from 3.10 onwards can only be decompiled by the Python version that wrote it
|
|
32
|
+
([#94](https://github.com/sahebbiswas/pycrefine/issues/94)). pycrefine tells you which
|
|
33
|
+
version to use, and [Getting started](https://github.com/sahebbiswas/pycrefine/blob/main/docs/getting-started.md#run-under-the-python-that-wrote-the-file)
|
|
34
|
+
shows how to run it under another Python in one command. Some constructs (`match`,
|
|
35
|
+
`async`/`await`, `yield from`) are not reconstructed yet; see
|
|
36
|
+
[Limitations](https://github.com/sahebbiswas/pycrefine/blob/main/docs/limitations.md).
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m pip install pycrefine
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Until the first release reaches PyPI, install from GitHub instead:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m pip install "git+https://github.com/sahebbiswas/pycrefine"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or run it from a checkout without installing: `python pycrefine.py file.pyc`. Install
|
|
51
|
+
problems (`command not found`, `externally-managed-environment`, …) are covered in
|
|
52
|
+
[Troubleshooting](https://github.com/sahebbiswas/pycrefine/blob/main/docs/troubleshooting.md#installation-and-setup).
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pycrefine module.cpython-312.pyc # print the source
|
|
58
|
+
pycrefine module.cpython-312.pyc -o module.py # save it to a file
|
|
59
|
+
python -m pycrefine module.cpython-312.pyc # same, if the command is not on PATH
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
From Python:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import pycrefine
|
|
66
|
+
|
|
67
|
+
source = pycrefine.get_decompiler("module.cpython-312.pyc").decompile()
|
|
68
|
+
print(source)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
| Guide | Read it to |
|
|
74
|
+
|---|---|
|
|
75
|
+
| [Getting started](https://github.com/sahebbiswas/pycrefine/blob/main/docs/getting-started.md) | install, decompile a first file, and run under the right Python version |
|
|
76
|
+
| [Use cases](https://github.com/sahebbiswas/pycrefine/blob/main/docs/use-cases.md) | recover a project from `__pycache__`, batch-decompile a tree, decompile PyInstaller or in-memory code, check the output |
|
|
77
|
+
| [Command line and Python API](https://github.com/sahebbiswas/pycrefine/blob/main/docs/usage.md) | look up every option, exit status and API call |
|
|
78
|
+
| [Troubleshooting](https://github.com/sahebbiswas/pycrefine/blob/main/docs/troubleshooting.md) | fix setup problems and understand each error message |
|
|
79
|
+
| [Limitations](https://github.com/sahebbiswas/pycrefine/blob/main/docs/limitations.md) | see what is not reconstructed yet, and how to tell |
|
|
80
|
+
| [Architecture](https://github.com/sahebbiswas/pycrefine/blob/main/docs/architecture.md) | find your way around the code |
|
|
81
|
+
| [Contributing](https://github.com/sahebbiswas/pycrefine/blob/main/CONTRIBUTING.md) | set up a development environment, run the checks, version and release |
|
|
82
|
+
|
|
83
|
+
## Contributing
|
|
84
|
+
|
|
85
|
+
Bug reports with the `.pyc` file (or the source it came from) and the Python version that
|
|
86
|
+
compiled it are the most useful contribution. See [CONTRIBUTING.md](https://github.com/sahebbiswas/pycrefine/blob/main/CONTRIBUTING.md) for
|
|
87
|
+
the development setup, the checks every pull request runs, and the versioning policy.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
[MIT](https://github.com/sahebbiswas/pycrefine/blob/main/LICENSE)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pycrefine"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A Python .pyc decompiler for educational purposes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Saheb"}]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = ["decompiler", "bytecode", "pyc", "cpython", "reverse-engineering"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Software Development :: Disassemblers",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [] # Keeps your "no dependencies" promise
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/sahebbiswas/pycrefine"
|
|
32
|
+
Issues = "https://github.com/sahebbiswas/pycrefine/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
test = ["pytest"]
|
|
36
|
+
dev = ["pytest", "pylint==4.1.3", "mypy==2.4.0"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
pycrefine = "pycrefine.cli:main" # Allows running 'pycrefine' directly in terminal
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.dynamic]
|
|
42
|
+
version = {attr = "pycrefine.__version__"}
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
python_files = "test_*.py"
|
|
50
|
+
|
|
51
|
+
# Type checking (docs/linting.md). Permissive by default: errors are ignored everywhere
|
|
52
|
+
# except the modules listed in the override below, which are checked with the strict
|
|
53
|
+
# flag set. Move a module into the list once it is clean. Run: mypy
|
|
54
|
+
[tool.mypy]
|
|
55
|
+
python_version = "3.10" # oldest target mypy supports; the code itself stays 3.9-compatible
|
|
56
|
+
files = ["src/pycrefine"]
|
|
57
|
+
mypy_path = "src"
|
|
58
|
+
ignore_errors = true
|
|
59
|
+
warn_unused_configs = true
|
|
60
|
+
warn_redundant_casts = true
|
|
61
|
+
|
|
62
|
+
[[tool.mypy.overrides]]
|
|
63
|
+
module = [
|
|
64
|
+
"pycrefine._constants",
|
|
65
|
+
"pycrefine.cli",
|
|
66
|
+
"pycrefine.codeobj",
|
|
67
|
+
"pycrefine.expr",
|
|
68
|
+
"pycrefine.instructions",
|
|
69
|
+
"pycrefine.versions",
|
|
70
|
+
]
|
|
71
|
+
ignore_errors = false
|
|
72
|
+
disallow_any_generics = true
|
|
73
|
+
disallow_untyped_calls = true
|
|
74
|
+
disallow_untyped_defs = true
|
|
75
|
+
disallow_incomplete_defs = true
|
|
76
|
+
check_untyped_defs = true
|
|
77
|
+
no_implicit_optional = true
|
|
78
|
+
warn_unused_ignores = true
|
|
79
|
+
warn_return_any = true
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""pycrefine: a Python .pyc decompiler that rebuilds source code from compiled bytecode.
|
|
2
|
+
|
|
3
|
+
Every name that used to live in the single-file ``pycrefine`` module is re-exported here,
|
|
4
|
+
so ``import pycrefine`` and ``from pycrefine import ...`` behave as before.
|
|
5
|
+
"""
|
|
6
|
+
__version__ = "1.1.1"
|
|
7
|
+
|
|
8
|
+
from ._constants import ( # noqa: F401
|
|
9
|
+
_AUG_ASSIGN_MAP,
|
|
10
|
+
_COMPOUND_EXPR_OPS,
|
|
11
|
+
_COMPOUND_SKIP,
|
|
12
|
+
_GENERIC_SKIP_OPS,
|
|
13
|
+
_INPLACE_ASSIGN_MAP,
|
|
14
|
+
_IS_COMP_GEN_SKIP,
|
|
15
|
+
_IS_COMP_GEN_STOP,
|
|
16
|
+
_MAX_TERNARY_MINI_SEARCH_WINDOW,
|
|
17
|
+
_MAX_TERNARY_SEARCH_WINDOW,
|
|
18
|
+
_MAX_TERNARY_SHORT_SEARCH_WINDOW,
|
|
19
|
+
_MAX_WITH_EXIT_SEARCH_WINDOW,
|
|
20
|
+
_NO_INDENT_TYPES,
|
|
21
|
+
_NO_PASS_TYPES,
|
|
22
|
+
_STATEMENT_BOUNDARY_OPS,
|
|
23
|
+
_TERNARY_PURE,
|
|
24
|
+
_TERNARY_SKIP,
|
|
25
|
+
_TERNARY_STORES,
|
|
26
|
+
_TERNARY_TERM,
|
|
27
|
+
_TRY_HANDLER_EXIT_OPS,
|
|
28
|
+
_TRY_LOAD_OPS,
|
|
29
|
+
_TRY_LOOP_HEADS,
|
|
30
|
+
_TRY_SKIP_NOP,
|
|
31
|
+
_UNARY_SYMBOLS,
|
|
32
|
+
_WHILE_TRIVIAL_OPS,
|
|
33
|
+
)
|
|
34
|
+
from .expr import StackNULL, _BoolExpr, _CollectionAccumSentinel, _unary_operand # noqa: F401
|
|
35
|
+
from .instructions import ( # noqa: F401
|
|
36
|
+
BytecodeInstruction,
|
|
37
|
+
_const_repr,
|
|
38
|
+
_is_anonymous_func_body,
|
|
39
|
+
_render_func_tuple,
|
|
40
|
+
)
|
|
41
|
+
from .postprocess import ( # noqa: F401
|
|
42
|
+
_block_opener_keyword,
|
|
43
|
+
_collect_multiline_header,
|
|
44
|
+
_line_is_in_triple_quoted_string,
|
|
45
|
+
collapse_chained_comparisons,
|
|
46
|
+
flatten_elif,
|
|
47
|
+
post_process_source,
|
|
48
|
+
)
|
|
49
|
+
from .codeobj import ForeignCode, _is_code # noqa: F401
|
|
50
|
+
from .decompiler.base import DecompilerBase # noqa: F401
|
|
51
|
+
from .decompiler.generic import DecompilerGeneric, _pick_decompiler_class # noqa: F401
|
|
52
|
+
from .opcodes.py39 import _OPCODES_39 # noqa: F401
|
|
53
|
+
from .decompiler.py39 import Decompiler39 # noqa: F401
|
|
54
|
+
from .decompiler.py311 import Decompiler311Plus # noqa: F401
|
|
55
|
+
from .decompiler.py314 import Decompiler314 # noqa: F401
|
|
56
|
+
from .loader import ( # noqa: F401
|
|
57
|
+
MarshalParser,
|
|
58
|
+
_CO_FAST_CELL,
|
|
59
|
+
_CO_FAST_FREE,
|
|
60
|
+
_CO_FAST_LOCAL,
|
|
61
|
+
_MARSHAL_NULL,
|
|
62
|
+
get_decompiler,
|
|
63
|
+
)
|
|
64
|
+
from .versions import _get_python_version_from_magic # noqa: F401
|
|
65
|
+
from .cli import main # noqa: F401
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Shared constant instruction sets, lookup maps and search-window sizes."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# --- Magic Numbers & Search Windows ---
|
|
5
|
+
_MAX_WITH_EXIT_SEARCH_WINDOW = 200
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_MAX_TERNARY_SEARCH_WINDOW = 20
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_MAX_TERNARY_SHORT_SEARCH_WINDOW = 12
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_MAX_TERNARY_MINI_SEARCH_WINDOW = 8
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- Constant Instruction Sets ---
|
|
18
|
+
_NO_PASS_TYPES = frozenset({"exc_cleanup", "finally_wrapper"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_NO_INDENT_TYPES = frozenset({"finally_wrapper"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_UNARY_SYMBOLS = {
|
|
25
|
+
"UNARY_NEGATIVE": "-", "UNARY_POSITIVE": "+", "UNARY_INVERT": "~", "UNARY_NOT": "not ",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_TERNARY_STORES = frozenset(("STORE_FAST", "STORE_NAME", "STORE_GLOBAL", "STORE_DEREF"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_TERNARY_SKIP = frozenset(("CACHE", "RESUME", "NOT_TAKEN", "COPY_FREE_VARS"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_TERNARY_TERM = frozenset(("RETURN_CONST", "RETURN_VALUE"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_TERNARY_PURE = frozenset((
|
|
39
|
+
"LOAD_FAST", "LOAD_NAME", "LOAD_GLOBAL", "LOAD_CONST", "LOAD_DEREF",
|
|
40
|
+
"LOAD_SMALL_INT", "LOAD_GLOBAL_MODULE", "LOAD_ATTR", "LOAD_METHOD",
|
|
41
|
+
"GET_ATTR", "LOAD_FAST_BORROW", "LOAD_FAST_BORROW_LOAD_FAST_BORROW",
|
|
42
|
+
"LOAD_CONST_BORROW", "CALL", "CALL_FUNCTION", "CALL_METHOD",
|
|
43
|
+
"COMPARE_OP", "BINARY_OP", "IS_OP", "CONTAINS_OP",
|
|
44
|
+
"UNARY_NOT", "UNARY_NEGATIVE", "UNARY_POSITIVE", "UNARY_INVERT", "CALL_INTRINSIC_1",
|
|
45
|
+
"BUILD_TUPLE", "BUILD_LIST", "BUILD_SET", "BUILD_MAP",
|
|
46
|
+
"PRECALL", "PUSH_NULL", "FORMAT_VALUE", "FORMAT_SIMPLE", "BUILD_STRING",
|
|
47
|
+
"BINARY_SUBSCR", "TO_BOOL", "BINARY_SLICE", "BUILD_SLICE",
|
|
48
|
+
# Python 3.9 named binary opcodes (3.11+ uses BINARY_OP instead):
|
|
49
|
+
"BINARY_ADD", "BINARY_SUBTRACT", "BINARY_MULTIPLY", "BINARY_TRUE_DIVIDE",
|
|
50
|
+
"BINARY_FLOOR_DIVIDE", "BINARY_MODULO", "BINARY_POWER", "BINARY_LSHIFT",
|
|
51
|
+
"BINARY_RSHIFT", "BINARY_AND", "BINARY_OR", "BINARY_XOR",
|
|
52
|
+
"BINARY_MATRIX_MULTIPLY",
|
|
53
|
+
)) | _TERNARY_SKIP | _TERNARY_TERM
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_COMPOUND_SKIP = frozenset(("CACHE", "RESUME", "NOT_TAKEN", "COPY_FREE_VARS", "PRECALL", "PUSH_NULL"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_COMPOUND_EXPR_OPS = frozenset((
|
|
60
|
+
"LOAD_FAST", "LOAD_NAME", "LOAD_GLOBAL", "LOAD_CONST", "LOAD_DEREF",
|
|
61
|
+
"LOAD_SMALL_INT", "LOAD_FAST_BORROW", "LOAD_CONST_BORROW", "LOAD_FAST_BORROW_LOAD_FAST_BORROW",
|
|
62
|
+
"LOAD_GLOBAL_MODULE", "LOAD_ATTR", "LOAD_METHOD", "GET_ATTR",
|
|
63
|
+
"CALL", "CALL_FUNCTION", "CALL_METHOD", "COMPARE_OP", "BINARY_OP", "IS_OP", "CONTAINS_OP",
|
|
64
|
+
"UNARY_NOT", "UNARY_NEGATIVE", "UNARY_POSITIVE", "UNARY_INVERT",
|
|
65
|
+
"BUILD_TUPLE", "BUILD_LIST", "BUILD_SET", "BUILD_MAP",
|
|
66
|
+
"TO_BOOL", "FORMAT_VALUE", "FORMAT_SIMPLE", "BUILD_STRING", "BINARY_SUBSCR", "BINARY_SLICE", "BUILD_SLICE",
|
|
67
|
+
"DUP_TOP", "DUP_TOP_TWO", "ROT_TWO", "ROT_THREE", "ROT_FOUR", "SWAP", "COPY",
|
|
68
|
+
# Python 3.9 binary opcodes:
|
|
69
|
+
"BINARY_ADD", "BINARY_SUBTRACT", "BINARY_MULTIPLY", "BINARY_TRUE_DIVIDE",
|
|
70
|
+
"BINARY_FLOOR_DIVIDE", "BINARY_MODULO", "BINARY_POWER", "BINARY_LSHIFT",
|
|
71
|
+
"BINARY_RSHIFT", "BINARY_AND", "BINARY_OR", "BINARY_XOR",
|
|
72
|
+
"BINARY_MATRIX_MULTIPLY"
|
|
73
|
+
)) | _COMPOUND_SKIP
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_WHILE_TRIVIAL_OPS = frozenset({"NOP", "RESUME", "NOT_TAKEN", "CACHE", "COPY_FREE_VARS"})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_STATEMENT_BOUNDARY_OPS = frozenset({
|
|
80
|
+
"POP_TOP", "POP_ITER", "END_FOR", "RETURN_VALUE", "RETURN_CONST", "RAISE_VARARGS",
|
|
81
|
+
"RERAISE", "POP_BLOCK", "PUSH_EXC_INFO", "POP_EXCEPT",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_TRY_SKIP_NOP = frozenset({"RESUME", "CACHE", "NOT_TAKEN"})
|
|
86
|
+
|
|
87
|
+
# Placeholders between a protected range's end and the code that follows it (a with
|
|
88
|
+
# block's inlined exit call, a finally copy): skipped when matching positions.
|
|
89
|
+
_FILLER_OPS = frozenset({"NOP", "CACHE"})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_TRY_LOAD_OPS = frozenset({"LOAD_NAME", "LOAD_GLOBAL", "LOAD_FAST",
|
|
93
|
+
"LOAD_DEREF", "LOAD_SMALL_INT", "LOAD_GLOBAL_MODULE"})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_TRY_LOOP_HEADS = frozenset({"FOR_ITER", "GET_ITER", "GET_ANEXT", "GET_AWAITABLE"})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_TRY_HANDLER_EXIT_OPS = frozenset({"POP_EXCEPT", "DELETE_FAST", "DELETE_NAME",
|
|
100
|
+
"DELETE_GLOBAL", "STORE_FAST", "STORE_NAME"})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_IS_COMP_GEN_SKIP = frozenset({"POP_TOP", "CACHE", "NOP", "RESUME", "COPY", "NOT_TAKEN"})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_IS_COMP_GEN_STOP = frozenset({"LOAD_CONST", "LOAD_NAME", "LOAD_FAST", "LOAD_GLOBAL", "LOAD_DEREF", "LOAD_SMALL_INT",
|
|
107
|
+
"LOAD_ATTR", "PUSH_NULL", "RERAISE", "RAISE_VARARGS", "RETURN_CONST", "RETURN_VALUE", "JUMP_FORWARD"})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_GENERIC_SKIP_OPS = frozenset({"RESUME", "CACHE", "NOP", "NOT_TAKEN", "COPY_FREE_VARS", "EXTENDED_ARG"})
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --- Augmented Assignment Opcodes ---
|
|
114
|
+
_AUG_ASSIGN_MAP = {
|
|
115
|
+
13: "+=", 14: "&=", 15: "//=", 16: "<<=", 17: "@=",
|
|
116
|
+
18: "*=", 19: "%=", 20: "|=", 21: "**=", 22: ">>=",
|
|
117
|
+
23: "-=", 24: "/=", 25: "^=",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_INPLACE_ASSIGN_MAP = {
|
|
122
|
+
"INPLACE_ADD": "+=", "INPLACE_SUBTRACT": "-=",
|
|
123
|
+
"INPLACE_MULTIPLY": "*=", "INPLACE_TRUE_DIVIDE": "/=",
|
|
124
|
+
"INPLACE_FLOOR_DIVIDE": "//=", "INPLACE_MODULO": "%=",
|
|
125
|
+
"INPLACE_POWER": "**=", "INPLACE_LSHIFT": "<<=",
|
|
126
|
+
"INPLACE_RSHIFT": ">>=", "INPLACE_AND": "&=",
|
|
127
|
+
"INPLACE_OR": "|=", "INPLACE_XOR": "^=",
|
|
128
|
+
"INPLACE_MATRIX_MULTIPLY": "@=",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --- Value-pushing opcodes (prefixes) ---
|
|
133
|
+
# Instructions that push a value, such as the callee of a call before its arguments;
|
|
134
|
+
# an inlined `with` exit call never directly follows one of them.
|
|
135
|
+
_VALUE_OPS = ("LOAD_", "PUSH_NULL", "CALL", "BINARY_", "BUILD_", "COPY", "GET_", "UNARY_",
|
|
136
|
+
"COMPARE_", "FORMAT_", "MAKE_FUNCTION", "CONTAINS_OP", "IS_OP")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Command-line interface."""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
from .loader import get_decompiler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# CLI
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description="A Python .pyc decompiler that reconstructs readable source code from compiled bytecode."
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("input", help="The .pyc file to decompile")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"-o", "--output",
|
|
19
|
+
help="Optional filename to save the decompiled output to. If not supplied, prints to screen."
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--beautification-level",
|
|
23
|
+
choices=['none', 'core', 'aggressive'], default='core',
|
|
24
|
+
help="Level of code beautification to apply. 'none' does no beautification. 'core' collapses else-if to elif. 'aggressive' is a stub for future use."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
decompiler = get_decompiler(args.input, args.beautification_level)
|
|
31
|
+
output_text = decompiler.decompile()
|
|
32
|
+
|
|
33
|
+
if args.output:
|
|
34
|
+
try:
|
|
35
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
36
|
+
f.write(output_text)
|
|
37
|
+
print(f"Decompiled output saved to {args.output}", file=sys.stderr)
|
|
38
|
+
except OSError as e:
|
|
39
|
+
print(f"Error: Failed to save decompiled output to {args.output}: {e}", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
else:
|
|
42
|
+
if output_text and output_text.strip():
|
|
43
|
+
print(output_text)
|
|
44
|
+
else:
|
|
45
|
+
print(f"Warning: Decompiler returned no source code for {args.input}", file=sys.stderr)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
except ValueError as e:
|
|
49
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
except Exception:
|
|
52
|
+
traceback.print_exc()
|
|
53
|
+
sys.exit(1)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Code-object helpers shared by the loader and the decompilers (``ForeignCode`` for foreign-version .pyc files)."""
|
|
2
|
+
import types
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ForeignCode:
|
|
7
|
+
"""
|
|
8
|
+
A code object read from a .pyc of a different Python version.
|
|
9
|
+
|
|
10
|
+
The host interpreter cannot build a real ``types.CodeType`` from another
|
|
11
|
+
version's bytecode, so the parsed fields are kept as plain ``co_*``
|
|
12
|
+
attributes. The decompilers only read those attributes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
co_name: str
|
|
16
|
+
|
|
17
|
+
def __init__(self, **fields: Any):
|
|
18
|
+
self.__dict__.update(fields)
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return f"<ForeignCode {self.co_name!r}>"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_code(obj: Any) -> bool:
|
|
25
|
+
"""True for a host code object or one parsed from another Python version."""
|
|
26
|
+
return isinstance(obj, (types.CodeType, ForeignCode))
|
|
File without changes
|