Metadata-Version: 2.4
Name: ppobj
Version: 1.0.0
Summary: A modern pprint alternative — pretty-print any Python object as clean JSON, with zero dependencies
Project-URL: Homepage, https://github.com/centi123/ppobj
Project-URL: Repository, https://github.com/centi123/ppobj
Project-URL: Issues, https://github.com/centi123/ppobj/issues
Author-email: centi123 <19553367+centi123@users.noreply.github.com>
License: MIT
License-File: LICENSE
Keywords: debugging,inspect,json,object,pprint,pretty-print,serialization
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Debuggers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# ppobj

**A modern alternative to `pprint`. Pretty-print any Python object as clean, formatted JSON — including custom classes, HTTP responses, and objects that crash standard serializers.**

```bash
pip install ppobj
```

## Why ppobj?

Python's `pprint` handles standard containers and dataclasses well, but it can't look inside custom objects (just shows `<object at 0x...>`), can't consume generators, can't parse HTTP response bodies, and crashes on objects with broken `__repr__`. Its output is Python repr syntax — not valid JSON, not pasteable into APIs or config files.

`ppobj` solves all of this:

- **Inspects any object** — extracts `__dict__` and `__slots__` attributes recursively, no `__repr__` needed
- **Valid JSON output** — double quotes, `true`/`false`/`null`, pasteable and parseable everywhere
- **Never crashes** — unserializable fields become descriptive placeholders, not exceptions
- **Zero dependencies** — pure stdlib at runtime
- **Two lines of code** — `from ppobj import pp` then `pp(anything)`

## Quick Start

```python
from ppobj import pp

pp({"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]})
```

```json
{
  "name": "Alice",
  "age": 30,
  "hobbies": [
    "reading",
    "coding"
  ]
}
```

## ppobj vs pprint

See the full side-by-side comparisons in [`examples/vs_pprint/`](examples/vs_pprint/).

### Custom objects

`pprint` shows `<object at 0x...>`. ppobj extracts the actual attributes:

```python
class Config:
    def __init__(self):
        self.host = "localhost"
        self.port = 5432
        self.debug = True
```

```
pprint ➜  <__main__.Config object at 0x7f3a...>

ppobj  ➜  {
            "__type__": "Config",
            "host": "localhost",
            "port": 5432,
            "debug": true
          }
```

The `__type__` key is included by default so you know what class the object is. To omit it:

```python
printer = PrettyPrinter(show_type=False)
printer.pp(Config())
# {"host": "localhost", "port": 5432, "debug": true}
```

### Generators

`pprint` shows `<generator object ...>`. ppobj consumes and shows the values:

```python
pp(x ** 2 for x in range(5))
```

```
pprint ➜  <generator object <genexpr> at 0x7f3a...>

ppobj  ➜  [0, 1, 4, 9, 16]
```

### JSON output with compact mode

`pprint` outputs Python repr syntax (single quotes, `True`/`False`/`None`) — not valid JSON. ppobj outputs standard JSON, with optional width-aware compaction:

```python
pp({"name": "Alice", "age": 30}, compact=True)
# {"name": "Alice", "age": 30}    ← valid JSON, pasteable anywhere

pp(large_nested_object, width=60)
# Outer structure expands, leaf dicts/lists stay compact
```

### Safety

`pprint` crashes on objects with broken `__repr__`. ppobj never crashes:

```python
class Broken:
    def __init__(self):
        self.value = 42
    def __repr__(self):
        raise RuntimeError("repr is broken!")

pp(Broken())
# {"__type__": "Broken", "value": 42}
```

Run all comparison examples with:

```bash
uv run python examples/vs_pprint/01_custom_objects.py
uv run python examples/vs_pprint/02_compact_formatting.py
uv run python examples/vs_pprint/03_dataclasses_enums.py
uv run python examples/vs_pprint/04_safety_and_edge_cases.py
uv run python examples/vs_pprint/05_http_responses.py        # requires network
uv run python examples/vs_pprint/06_generators_iterators.py
```

## HTTP Responses in Seconds

Debugging API calls? Just `pp()` the response — headers, parsed JSON body, status, timing, all laid out:

```python
import httpx
from ppobj import pp

response = httpx.get("https://httpbin.org/json")
pp(response)
```

```json
{
  "__type__": "httpx.Response",
  "status_code": 200,
  "url": "https://httpbin.org/json",
  "reason_phrase": "OK",
  "http_version": "HTTP/1.1",
  "is_redirect": false,
  "headers": {
    "content-type": "application/json"
  },
  "body": {
    "slideshow": {
      "author": "Yours Truly",
      "title": "Sample Slide Show"
    }
  },
  "elapsed": "0:00:00.900709"
}
```

Works with both `httpx` and `requests` — no extra imports needed.

## API

```python
from ppobj import pp, to_json, to_dict

# Print to stdout (returns the original object for inline debugging)
result = process(pp(data))

# Compact mode — collapses small structures onto one line
pp(obj, compact=True)
pp(obj, width=60)

# Formatting options
pp(obj, indent=4, sort_keys=True)

# Get as JSON string
json_str = to_json(obj)

# Get as dict/list/primitive for further processing
data = to_dict(obj)
```

### PrettyPrinter class

For reusable, configured output:

```python
from ppobj import PrettyPrinter

printer = PrettyPrinter(
    indent=4,
    width=100,
    compact=True,
    max_depth=10,
    max_items=500,
    max_string_length=5000,
    sort_keys=True,
)
printer.pp(my_object)
```

### Custom handlers

Extend ppobj with your own type serializers:

```python
from ppobj import BaseHandler, PrettyPrinter

class MyTypeHandler(BaseHandler):
    def can_handle(self, obj):
        return isinstance(obj, MySpecialType)

    def serialize(self, obj, *, recurse, context):
        return {"custom_field": recurse(obj.data)}

printer = PrettyPrinter()
printer.registry.prepend(MyTypeHandler())  # highest priority
```

## Supported Types

| Category | Types |
|---|---|
| Primitives | `str`, `int`, `float`, `bool`, `None` |
| Containers | `dict`, `list`, `tuple`, `set`, `frozenset` |
| Collections | `OrderedDict`, `defaultdict`, `Counter`, `deque`, `namedtuple` |
| Date/Time | `datetime`, `date`, `time`, `timedelta` |
| Enums | `Enum`, `IntEnum`, `StrEnum`, `Flag` |
| Dataclasses | `@dataclass` (including frozen and slotted) |
| Exceptions | All `Exception` subclasses with chaining info |
| HTTP | `httpx.Response`, `requests.Response` |
| Pydantic | `BaseModel` v1 and v2 |
| Stdlib | `Path`, `UUID`, `Decimal`, `re.Pattern`, `bytes`, `complex`, `range` |
| Iterators | Generators, `map`, `filter`, `zip`, custom iterators |
| Custom | Any object with `__dict__` or `__slots__` |

No library imports required — ppobj detects types by structure, not by importing the library.

## Graceful Error Handling

ppobj never raises on bad data. When a field can't be serialized, you get a clear placeholder:

```json
{
  "normal_field": "works fine",
  "problematic_field": "<unserializable: SomeType, len=3, repr=<SomeType object>>"
}
```

Other safety placeholders:

| Placeholder | Meaning |
|---|---|
| `<circular reference: ClassName>` | Circular reference detected |
| `<max depth (20) exceeded: ClassName>` | Nesting too deep |
| `<string: 5000 chars, truncated>` | String exceeded `max_string_length` |
| `<... 990 more items (total: 1000)>` | Collection exceeded `max_items` |
| `<not yet consumed>` | HTTP response body not read yet |
| `<stream not consumed>` | Streaming response not consumed |

## Requirements

- Python 3.10+
- Zero runtime dependencies

## License

MIT
