Metadata-Version: 2.4
Name: scramblenames
Version: 0.1.0
Summary: Obfuscate Python source code by scrambling variable, function, and class names into meaningless identifiers while keeping the program working.
Author-email: Al Sweigart <asweigart@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/asweigart/scramblenames
Keywords: obfuscation,obfuscator,scramble,rename,identifiers,minify,source-code,libcst,codemod
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: Pre-processors
Classifier: Topic :: Security
Classifier: License :: OSI Approved :: MIT License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: libcst>=1.0
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"

# scramblenames

Obfuscate Python source code by scrambling variable, function, and class names
into meaningless eight-character identifiers, while keeping the program working
exactly the same. Comments and docstrings are removed by default too. The point
is to strip every human-readable clue from a program without changing what it
does.

It does **not** un-scramble — this is a one-way transform.

## Install

```bash
pip install scramblenames
```

`scramblenames` uses [libcst](https://github.com/Instagram/LibCST) to preserve
formatting (and comments/docstrings when you choose to keep them).

## Library usage

```python
from scramblenames import scramble

source = '''
def add(a, b):
    total = a + b
    return total
'''

print(scramble(source, seed=1))
```

```python
def gFb51yBM(iK2ZWeqh, FWCEPyYn):
    WXaSCrUZ = iK2ZWeqh + FWCEPyYn
    return WXaSCrUZ
```

`scramble()` accepts the code as a string, a `pathlib.Path`, or a string
filename of an existing file:

```python
from pathlib import Path
scramble(Path("mymodule.py"))     # read from a Path
scramble("mymodule.py")           # read from a filename
scramble("x = 1\n")               # scramble a code string directly
```

### Scrambling a whole directory

`scramble_tree()` scrambles every matching file under a directory, mirroring the
tree into a destination directory:

```python
from scramblenames import scramble_tree

scramble_tree("myproject/", "obfuscated/", import_safe=True)
```

Each file is scrambled independently, so use `import_safe=True` for multi-file
packages — otherwise a public name renamed in one module wouldn't be matched at
its import site in another. Use `include`/`exclude` globs and `recursive=False`
to control which files are processed.

### Errors

Source that can't be parsed raises `scramblenames.ScrambleError`, carrying the
`filename`, `line`, and `column` of the problem when available.

### Options

```python
scramble(
    source,
    import_safe=False,       # keep top-level function/class/global names
    strip_comments=True,     # remove all comments
    strip_docstrings=True,   # remove module/function/class docstrings
    style="random",          # "random" | "hex" | "reorder"
    seed=None,               # int for reproducible output
)
```

| Option | Effect |
| --- | --- |
| `import_safe=True` | Assume the file may be imported by other code. Module-level function, class, and global names are **kept**; only function-local names are scrambled. |
| `import_safe=False` (default) | Also scramble module-level function, class, and global names (except those in `__all__`). Best for standalone scripts. |
| `strip_comments=True` (default) | Remove every comment. A first-line shebang (`#!...`) and a PEP 263 encoding cookie are always kept. Set `False` to keep all comments. |
| `strip_docstrings=True` (default) | Remove the leading docstring of the module and of every function and class (an empty body becomes `pass`). Set `False` to keep them. |
| `style="random"` | Eight random alphanumeric characters, first is always a letter. |
| `style="hex"` | Eight hexadecimal characters, first is always a letter (`a`–`f`). |
| `style="reorder"` | A permutation of the original name's own characters. |

## Command line

```bash
scramblenames mymodule.py                       # scramble to stdout (strips prose)
scramblenames mymodule.py -o out.py             # write to a file
scramblenames mymodule.py --import-safe         # keep the public API
scramblenames mymodule.py --keep-prose          # keep comments + docstrings
scramblenames mymodule.py --style hex --seed 7  # reproducible hex names
cat mymodule.py | scramblenames                 # read from stdin

# scramble a whole project into a mirrored output directory
scramblenames myproject/ -d obfuscated/ --import-safe
scramblenames src/ -d out/ --exclude '*_test.py' --no-recursive
```

Scrambling a directory (or more than one file) requires `-d/--output-dir`, which
mirrors the input tree. The paths written are listed on stderr. A parse error
prints a clean message and exits non-zero.

## What is and isn't scrambled

Every generated name is a valid Python identifier and never a keyword, so the
output is always syntactically valid and behaves identically to the input.

**Scrambled**

- Local variables inside functions.
- `for`, `with ... as`, `except ... as`, and walrus targets.
- Comprehension and generator variables.
- Module-level functions, classes, and globals — **only** when
  `import_safe=False`.
- Nested function and class names (they are local to their enclosing function).
- **Parameters that can't be reached by keyword** — positional-only params,
  `*args`, and `**kwargs` — always.
- **Other parameters**, when the function is safe to analyze: it's local (its
  own name is scrambled too), undecorated, never passed around or stored, and
  never called with `**` unpacking. Every keyword argument at its call sites is
  rewritten to match (`f(name=1)` → `f(x7Kd2p9q=1)`).

**Never scrambled** (renaming these could break the program)

- **Attributes** — `x.foo`, `self.foo`, and the string entries in `__slots__`,
  since the object's type generally can't be proven.
- **Method names** — reached by attribute access, like other attributes.
- **Method and public-API parameters** — outside callers may pass them by
  keyword, so keyword-eligible parameters of methods, of the public API, and of
  functions that escape the module are kept.
- **Imported names and builtins.**
- Names listed in `__all__`, and dunder names like `__init__`.

Because attributes are preserved and parameters are only renamed when all call
sites are visible, code that relies on dynamic access — `getattr`/`setattr`
with computed strings, `locals()`, `**kwargs` forwarding by string key — keeps
working.

## Development

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[test]"
pytest
```

## License

MIT
