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.
Files changed (45) hide show
  1. pycrefine-1.1.1/LICENSE +21 -0
  2. pycrefine-1.1.1/PKG-INFO +123 -0
  3. pycrefine-1.1.1/README.md +91 -0
  4. pycrefine-1.1.1/pyproject.toml +79 -0
  5. pycrefine-1.1.1/setup.cfg +4 -0
  6. pycrefine-1.1.1/src/pycrefine/__init__.py +65 -0
  7. pycrefine-1.1.1/src/pycrefine/__main__.py +3 -0
  8. pycrefine-1.1.1/src/pycrefine/_constants.py +136 -0
  9. pycrefine-1.1.1/src/pycrefine/cli.py +53 -0
  10. pycrefine-1.1.1/src/pycrefine/codeobj.py +26 -0
  11. pycrefine-1.1.1/src/pycrefine/decompiler/__init__.py +0 -0
  12. pycrefine-1.1.1/src/pycrefine/decompiler/base.py +258 -0
  13. pycrefine-1.1.1/src/pycrefine/decompiler/generic.py +5942 -0
  14. pycrefine-1.1.1/src/pycrefine/decompiler/py311.py +117 -0
  15. pycrefine-1.1.1/src/pycrefine/decompiler/py314.py +219 -0
  16. pycrefine-1.1.1/src/pycrefine/decompiler/py39.py +1269 -0
  17. pycrefine-1.1.1/src/pycrefine/expr.py +91 -0
  18. pycrefine-1.1.1/src/pycrefine/instructions.py +393 -0
  19. pycrefine-1.1.1/src/pycrefine/loader.py +342 -0
  20. pycrefine-1.1.1/src/pycrefine/opcodes/__init__.py +0 -0
  21. pycrefine-1.1.1/src/pycrefine/opcodes/py39.py +135 -0
  22. pycrefine-1.1.1/src/pycrefine/postprocess.py +494 -0
  23. pycrefine-1.1.1/src/pycrefine/versions.py +41 -0
  24. pycrefine-1.1.1/src/pycrefine.egg-info/PKG-INFO +123 -0
  25. pycrefine-1.1.1/src/pycrefine.egg-info/SOURCES.txt +43 -0
  26. pycrefine-1.1.1/src/pycrefine.egg-info/dependency_links.txt +1 -0
  27. pycrefine-1.1.1/src/pycrefine.egg-info/entry_points.txt +2 -0
  28. pycrefine-1.1.1/src/pycrefine.egg-info/requires.txt +8 -0
  29. pycrefine-1.1.1/src/pycrefine.egg-info/top_level.txt +1 -0
  30. pycrefine-1.1.1/tests/test_basic.py +1143 -0
  31. pycrefine-1.1.1/tests/test_control_flow.py +1195 -0
  32. pycrefine-1.1.1/tests/test_correctness.py +285 -0
  33. pycrefine-1.1.1/tests/test_exceptions.py +1212 -0
  34. pycrefine-1.1.1/tests/test_fallback.py +33 -0
  35. pycrefine-1.1.1/tests/test_foreign_pyc_fixtures.py +43 -0
  36. pycrefine-1.1.1/tests/test_helpers.py +475 -0
  37. pycrefine-1.1.1/tests/test_instruction_regressions.py +116 -0
  38. pycrefine-1.1.1/tests/test_integration.py +799 -0
  39. pycrefine-1.1.1/tests/test_internals.py +840 -0
  40. pycrefine-1.1.1/tests/test_lambda_decompilation.py +67 -0
  41. pycrefine-1.1.1/tests/test_lint_ratchet.py +186 -0
  42. pycrefine-1.1.1/tests/test_metrics.py +374 -0
  43. pycrefine-1.1.1/tests/test_strings.py +96 -0
  44. pycrefine-1.1.1/tests/test_subroutines.py +439 -0
  45. pycrefine-1.1.1/tests/test_version_bump_check.py +55 -0
@@ -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.
@@ -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
+ [![Python 3.9](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py39.yml/badge.svg?branch=main&event=push)](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py39.yml)
36
+ [![Python 3.12](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py312.yml/badge.svg?branch=main&event=push)](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py312.yml)
37
+ [![Python 3.14](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py314.yml/badge.svg?branch=main&event=push)](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py314.yml)
38
+ [![Lint](https://github.com/sahebbiswas/pycrefine/actions/workflows/lint.yml/badge.svg?branch=main&event=push)](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
+ [![Python 3.9](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py39.yml/badge.svg?branch=main&event=push)](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py39.yml)
4
+ [![Python 3.12](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py312.yml/badge.svg?branch=main&event=push)](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py312.yml)
5
+ [![Python 3.14](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py314.yml/badge.svg?branch=main&event=push)](https://github.com/sahebbiswas/pycrefine/actions/workflows/ci-py314.yml)
6
+ [![Lint](https://github.com/sahebbiswas/pycrefine/actions/workflows/lint.yml/badge.svg?branch=main&event=push)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -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