Skip to content

Commit 59e6365

Browse files
committed
lint improvements
1 parent 718a6bf commit 59e6365

17 files changed

+272
-74
lines changed

.github/workflows/package-test.yml

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
name: CI Test
1+
name: CI
2+
23
on:
34
push:
45
branches:
@@ -13,23 +14,50 @@ permissions:
1314
contents: read
1415

1516
jobs:
16-
Unit-Tests:
17-
timeout-minutes: 10
17+
lint:
18+
name: Lint & format
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.12"
27+
28+
- name: Install lint tools
29+
run: pip install ruff black isort pyright
30+
31+
- name: Ruff (lint)
32+
run: ruff check python_obfuscator tests
33+
34+
- name: Black (format check)
35+
run: black --check python_obfuscator tests
36+
37+
- name: isort (import order check)
38+
run: isort --check-only python_obfuscator tests
39+
40+
- name: Pyright (type check)
41+
run: pyright python_obfuscator
42+
43+
test:
44+
name: Test (Python ${{ matrix.python-version }})
1845
runs-on: ubuntu-latest
1946
strategy:
2047
fail-fast: false
2148
matrix:
2249
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
2350
steps:
2451
- uses: actions/checkout@v4
52+
2553
- name: Install Python ${{ matrix.python-version }}
2654
uses: actions/setup-python@v5
2755
with:
2856
python-version: ${{ matrix.python-version }}
57+
allow-prereleases: true
2958

3059
- name: Install Poetry
31-
run: |
32-
curl -sSL https://install.python-poetry.org | python - -y
60+
run: curl -sSL https://install.python-poetry.org | python - -y
3361

3462
- name: Update PATH
3563
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
@@ -40,13 +68,13 @@ jobs:
4068
- name: Type check (mypy)
4169
run: poetry run mypy python_obfuscator
4270

43-
- name: Run unit tests with coverage
71+
- name: Run tests with coverage
4472
run: |
4573
poetry run coverage run -m pytest
4674
poetry run coverage report
4775
poetry run coverage xml
4876
49-
- name: Upload coverage reports to Codecov
77+
- name: Upload coverage to Codecov
5078
if: matrix.python-version == '3.12'
5179
uses: codecov/codecov-action@v5
5280
with:

CHANGELOG.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6+
This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
---
9+
10+
## [0.1.0] — AST-Based Rewrite
11+
12+
Complete rewrite from a regex-based implementation to a fully AST-based pipeline.
13+
14+
### Added
15+
16+
- **`ObfuscationConfig`** — immutable frozen dataclass for selecting techniques.
17+
Factory methods: `all_enabled()`, `only(*names)`, `.without(*names)`, `.with_added(*names)`.
18+
- **Technique registry**`@register` class decorator; techniques self-register by name.
19+
`all_technique_names()` and `get_transforms(enabled)` for pipeline construction.
20+
- **`variable_renamer`** — two-pass AST renamer. First pass collects renameable names
21+
(excludes builtins, imports, dunders, and attribute-accessed names); second pass applies
22+
the mapping. Also renames `nonlocal` and `global` statement name lists.
23+
- **`string_hex_encoder`** — converts string literals to `bytes.fromhex(…).decode('utf-8')`
24+
call nodes. Skips f-strings where replacing a `Constant` with a `Call` is invalid.
25+
- **`dead_code_injector`** — recursively injects dead variable assignments at every scope
26+
level: module body, function bodies, class bodies, if/for/while/try/with branches.
27+
Some assignments reference earlier dead variables (intra-scope cross-references) to
28+
simulate computation. Accepts `InjectionParams` and a seeded `random.Random` for
29+
reproducible output.
30+
- **`exec_wrapper`** — wraps the entire module in `exec(ast.unparse(tree))`, reducing the
31+
top-level AST to one statement.
32+
- **`Obfuscator` class** — caches the transform pipeline across multiple `obfuscate()` calls.
33+
- **`obfuscate()` module-level helper** — convenience wrapper for one-shot use.
34+
- **CLI** (`pyobfuscate`) — `--disable/-d` flag accepts technique names; `--stdout`; `--version/-V`.
35+
- **E2E test suite** with correctness and benchmark tests across six complex programs.
36+
- **Per-technique runtime benchmarks** showing individual overhead contribution.
37+
- 100 % branch coverage enforced in CI.
38+
- Python 3.10–3.14 test matrix.
39+
40+
### Changed
41+
42+
- Priority ordering now encoded in `TechniqueMetadata.priority` rather than list position.
43+
- `VariableNameGenerator` and `RandomDataTypeGenerator` accept an optional `rng` argument
44+
for deterministic testing.
45+
- `_NameCollector` exposes `all_bound_names` (assigned + imported + builtins) for use by
46+
the dead-code injector's exclusion set.
47+
48+
### Removed
49+
50+
- All regex-based technique implementations (`techniques.py`).
51+
- `regex` runtime dependency (was used only for the old hex-encoding approach).
52+
- `SourceTransform` base class and `source_transforms` package.
53+
54+
### Fixed
55+
56+
- `VariableRenamer` now updates `nonlocal` and `global` statement name lists, preventing
57+
`SyntaxError: no binding for nonlocal '…' found` after renaming.
58+
- `VariableRenamer` excludes attribute-accessed names from renaming to prevent
59+
`AttributeError` when calling methods by the original name after their definition is renamed.
60+
- `_interleave` preserves relative ordering of injected statements (sorted random slots)
61+
so intra-scope cross-references never appear before their definitions.
62+
63+
---
64+
65+
## [0.0.2] — prior release
66+
67+
Initial public release with regex-based obfuscation techniques.
68+
69+
- `one_liner` — collapsed newlines into semicolons (superceded by `exec_wrapper`).
70+
- `variable_renamer` — regex-based name replacement.
71+
- `add_random_variables` — prepended/appended random assignments at module level.
72+
- `str_to_hex_bytes` — regex-based string-to-hex conversion.

README.md

Lines changed: 142 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,184 @@
1-
# Python-Obfuscator
1+
# python-obfuscator
22

3-
One night I got bored of writing good code, so I made good code to make bad code.
3+
[![CI](https://github.com/davidteather/python-obfuscator/actions/workflows/package-test.yml/badge.svg)](https://github.com/davidteather/python-obfuscator/actions/workflows/package-test.yml)
4+
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/davidteather/python-obfuscator?style=flat-square)](https://github.com/davidteather/python-obfuscator/releases)
5+
[![Downloads](https://static.pepy.tech/personalized-badge/python-obfuscator?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads)](https://pypi.org/project/python-obfuscator/)
6+
[![Codecov](https://codecov.io/gh/davidteather/python-obfuscator/branch/main/graph/badge.svg)](https://codecov.io/gh/davidteather/python-obfuscator)
47

5-
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/davidteather/python-obfuscator?style=flat-square)](https://github.com/davidteather/python-obfuscator/releases) [![Downloads](https://static.pepy.tech/personalized-badge/python-obfuscator?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads)](https://pypi.org/project/python-obfuscator/) ![](https://visitor-badge.laobi.icu/badge?page_id=davidteather.python-obfuscator) [![Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=flat-square&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/david-teather-4400a37a/)
8+
A Python source-code obfuscator built on the standard-library `ast` module. It applies multiple independent techniques — each individually togglable — to make code harder to read while keeping it fully executable. See [Known limitations](#known-limitations) before use.
69

7-
### **DONT USE IN PRODUCTION**
10+
If this project is useful to you, consider [sponsoring development](https://github.com/sponsors/davidteather).
811

9-
**I just made this because it was interesting to me. I do plan on making this more official in the future, but currently don't have the time!**
10-
11-
Consider sponsoring me [here](https://github.com/sponsors/davidteather)
12+
---
1213

1314
## Installing
1415

15-
```
16+
```bash
1617
pip install python-obfuscator
1718
```
1819

19-
## Quickstart
20+
Requires Python ≥ 3.10.
2021

21-
By default, obfuscated code is written under an **`obfuscated/`** directory (created next to your current working directory). Paths under the current directory are preserved (e.g. `src/app.py``obfuscated/src/app.py`).
22+
---
2223

23-
```
24-
pyobfuscate -i your_file.py
25-
```
24+
## Quick start — CLI
2625

27-
Print to stdout instead (e.g. for piping):
26+
```bash
27+
# Writes obfuscated/your_file.py (path structure is preserved)
28+
pyobfuscate -i your_file.py
2829

29-
```
30+
# Print to stdout
3031
pyobfuscate -i your_file.py --stdout
31-
```
3232

33-
Print the installed version (from `python_obfuscator.version`):
33+
# Disable specific techniques
34+
pyobfuscate -i your_file.py --disable dead_code_injector --disable exec_wrapper
3435

35-
```
36+
# Show version
3637
pyobfuscate --version
3738
```
3839

39-
## More Detailed Documentation
40+
---
41+
42+
## Python API
43+
44+
### One-shot helper
45+
46+
```python
47+
from python_obfuscator import obfuscate
48+
49+
source = "x = 1\nprint(x + 2)"
50+
result = obfuscate(source)
51+
print(result)
52+
```
53+
54+
### Selective techniques
55+
56+
```python
57+
from python_obfuscator import obfuscate, ObfuscationConfig
58+
59+
# All techniques except dead-code injection
60+
config = ObfuscationConfig.all_enabled().without("dead_code_injector")
61+
result = obfuscate(source, config=config)
4062

41-
You can use this as a module if you want
63+
# Only string encoding
64+
config = ObfuscationConfig.only("string_hex_encoder")
65+
result = obfuscate(source, config=config)
4266
```
43-
import python_obfuscator
44-
obfuscator = python_obfuscator.obfuscator()
4567

46-
code_to_obfuscate = "print('hello world')"
68+
### Reusing across multiple files (caches the pipeline)
69+
70+
```python
71+
from python_obfuscator import Obfuscator, ObfuscationConfig
72+
73+
obf = Obfuscator(ObfuscationConfig.all_enabled())
74+
for path in my_files:
75+
path.write_text(obf.obfuscate(path.read_text()))
4776
```
4877

49-
You can also exclude certain techniques applied for obfuscation
78+
### Config combinators
79+
80+
```python
81+
cfg = ObfuscationConfig.all_enabled() # every registered technique
82+
cfg = ObfuscationConfig.only("variable_renamer", "exec_wrapper")
83+
cfg = cfg.without("exec_wrapper") # returns a new frozen config
84+
cfg = cfg.with_added("dead_code_injector")
5085
```
51-
import python_obfuscator
52-
from python_obfuscator.techniques import add_random_variables
53-
obfuscator = python_obfuscator.obfuscator()
5486

55-
code_to_obfuscate = "print('hello world')"
56-
obfuscated_code = obfuscator.obfuscate(code_to_obfuscate, remove_techniques=[add_random_variables])
87+
---
88+
89+
## Techniques
90+
91+
| Name | Priority | What it does |
92+
|------|----------|--------------|
93+
| `variable_renamer` | 10 | Renames local variables, function names, parameters, and class names to visually ambiguous identifiers (`lIIllIlI…`). Excludes builtins, imports, dunders, and attribute-accessed names. |
94+
| `string_hex_encoder` | 20 | Replaces every string literal `"hi"` with `bytes.fromhex('6869').decode('utf-8')`. Skips f-strings. |
95+
| `dead_code_injector` | 30 | Injects dead variable assignments at **every scope level** — module body, function bodies, class bodies, if/for/while/try/with branches. Some assignments reference other dead variables to simulate computation. |
96+
| `exec_wrapper` | 100 | Wraps the entire module in a single `exec("…")` call, reducing the top-level AST to one statement. Runs last. |
97+
98+
Techniques are applied in priority order (lowest first).
99+
100+
---
101+
102+
## Example
103+
104+
**Input**
105+
106+
```python
107+
def greet(name):
108+
msg = "Hello, " + name
109+
print(msg)
110+
111+
greet("world")
57112
```
58-
Find a list of all techniques [here](https://github.com/davidteather/python-obfuscator/blob/210da2d3dfb96ab7653fad869a43cb67aeb0fe67/python_obfuscator/techniques.py#L87)
59113

60-
## Example Obfuscated Code
114+
**Output** (all techniques enabled — abridged)
61115

62-
Input
116+
```python
117+
exec('def lIlIllI(IIlIlII):\n lIllIlI = bytes.fromhex(\'48656c6c6f2c20\').decode(\'utf-8\') + IIlIlII\n ...\nlIlIllI(bytes.fromhex(\'776f726c64\').decode(\'utf-8\'))')
63118
```
64-
y = input("what's your favorite number")
65119

66-
user_value = int(y)
67-
print("{} that's a great number!".format(user_value))
120+
---
121+
122+
## Performance overhead
123+
124+
Benchmarks run on an Apple M-series machine, 20 iterations each. The test programs cover OOP, algorithms, functional patterns, number theory, and string processing.
125+
126+
### Total overhead (all techniques)
127+
128+
| Program | Original | Obfuscated | Overhead |
129+
|---------|----------|------------|---------|
130+
| `algorithms.py` | 0.94 ms | 1.98 ms | +112% |
131+
| `cipher.py` | 1.37 ms | 2.67 ms | +95% |
132+
| `data_structures.py` | 0.72 ms | 2.20 ms | +207% |
133+
| `functional.py` | 0.67 ms | 1.71 ms | +155% |
134+
| `number_theory.py` | 1.84 ms | 3.16 ms | +72% |
135+
| `oop_simulation.py` | 0.68 ms | 1.66 ms | +144% |
136+
137+
### Per-technique contribution (average across all programs)
138+
139+
| Technique | Avg overhead | Notes |
140+
|-----------|-------------|-------|
141+
| `variable_renamer` | ~5% | Pure rename — negligible at runtime |
142+
| `string_hex_encoder` | ~12% | `bytes.fromhex` call per string literal |
143+
| `dead_code_injector` | ~85% | Dominant cost — dead assignments execute every iteration |
144+
| `exec_wrapper` | ~2% | Single extra `exec` layer |
145+
146+
The dead-code injector's overhead scales with the number of scopes and loop iterations in the original program. Programs with tight inner loops see the most overhead.
147+
148+
---
149+
150+
## Known limitations
151+
152+
- **Class method names are not renamed.** Attribute-accessed names (`obj.method`) cannot be safely renamed without full type-inference, so the renamer conservatively excludes them.
153+
- **Keyword argument names are not renamed.** `fn(key=val)` call-site keyword strings are bare AST strings, not `Name` nodes, and are not updated when a parameter is renamed.
154+
- **No scope-aware renaming.** The same identifier used in two independent function scopes maps to the same obfuscated name (which is semantically correct but less obfuscated than it could be).
155+
- **No control-flow obfuscation.** Opaque predicates, bogus branches, and integer encoding are not implemented.
156+
157+
---
158+
159+
## Running the test suite
160+
161+
```bash
162+
pip install -e ".[dev]"
163+
pytest
68164
```
69165

70-
[With `pyobfuscate -i file.py`](https://gist.github.com/davidteather/b6ff932140d8c174b9c6f50c9b42fdaf)
166+
Coverage is enforced at ≥ 95% on every CI run.
71167

168+
```bash
169+
# With coverage report
170+
coverage run -m pytest && coverage report
72171

73-
[With `--one-liner`](https://gist.github.com/davidteather/75e48c04bf74f0262fe2919239a74295)
172+
# E2E tests with benchmark output
173+
pytest tests/e2e/ -v -s
174+
```
74175

75-
## Authors
176+
---
76177

77-
* **David Teather** - *Initial work* - [davidteather](https://github.com/davidteather)
178+
## Authors
78179

79-
See also the list of [contributors](https://github.com/davidteather/python-obfuscator) who participated in this project.
180+
**David Teather**[davidteather](https://github.com/davidteather)
80181

81182
## License
82183

83-
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
184+
MIT see [LICENSE](LICENSE).

0 commit comments

Comments
 (0)