Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
0.4.2 /
- Refactor `PrettyPrint` to delegate `format3DTorch` logic to `Formatter` for consistency
- Add and update tests in `FormatterTest`
- Refactor `Formatter` and `PrettyPrint` to use `Env::isCli()` for CLI detection
- introduce `Env` utility class with tests; update `FormatterTest` to validate new behavior
- Add support for custom separators in `PrettyPrint`; update tests to cover new functionality

0.4.1 / 2025-12-02
- Enhance `PrettyPrint::formatValue` to handle objects, resources, and unknown types
- Revise `PrettyPrint` to output top-level strings without quotes
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ pprint('Add 2 lines', end: "\n\n");
pprint('Tabbed', start: "\t");
// Combine with end to avoid newline
pprint('Prompted', start: '>>> ', end: '');

// Custom separator between multiple values (default is a single space " ")
pprint('A', 'B', 'C', sep: ', ', end: '');
// A, B, C

// Separator can also be provided via trailing options array
pprint('X', 'Y', ['sep' => "\n", 'end' => '']);
// X
// Y
```

Print and then exit the script
Expand Down Expand Up @@ -159,6 +168,7 @@ Notes:

- **start**: string. Prefix printed before the content. Example: `pprint('Hello', ['start' => "\t"])`.
- **end**: string. Line terminator, default to new line. Example: `pprint('no newline', ['end' => '']);`
- **sep**: string. Separator between multiple default-formatted arguments. Default is a single space `' '`. Examples: `pprint('A','B','C', sep: ', ', end: '')` or `pprint('X','Y', ['sep' => "\n", 'end' => ''])`.
- **label**: string. Prefix label for 2D/3D formatted arrays, default `tensor`. Example: `pprint($m, ['label' => 'arr'])`.
- **precision**: int. Number of digits after the decimal point for floats. Example: `pprint(3.14159, precision: 2)` prints `3.14`.
- **headB / tailB**: ints. Number of head/tail 2D blocks shown for 3D tensors.
Expand All @@ -171,6 +181,7 @@ All options can be passed as:

#### Defaults
- **label**: `tensor`
- **sep**: `' '`
- **precision**: `4`
- **headB / tailB**: `5`
- **headRows / tailRows**: `5`
Expand Down
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<rule ref="PSR12"/>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="200"/>
<property name="lineLimit" value="250"/>
<property name="absoluteLineLimit" value="0"/>
<property name="ignoreComments" value="true"/>
</properties>
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@
<html outputDirectory="build/coverage" lowUpperBound="50" highLowerBound="90"/>
</report>
</coverage>
<php>
<env name="APP_ENV" value="test"/>
</php>
</phpunit>
38 changes: 38 additions & 0 deletions src/Env.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Apphp\PrettyPrint;

/**
* Environment utilities.
*/
class Env
{
// Used for testing purposes only
private static ?bool $cliOverride = null;

/**
* Check if the application is running in CLI mode.
* @return bool
*/
public static function isCli(): bool
{
if (self::$cliOverride !== null) {
return self::$cliOverride;
}
return (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
}

/**
* Check if the application is running in test mode.
* @return bool
*/
public static function isTest(): bool
{
return (PHP_SAPI === 'cli' && getenv('APP_ENV') === 'test');
}

public static function setCliOverride(?bool $value): void
{
self::$cliOverride = $value;
}
}
60 changes: 59 additions & 1 deletion src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public static function formatCell(mixed $cell, int $precision): string
if (is_int($cell) || is_float($cell)) {
$s = self::formatNumber($cell, $precision);
} elseif (is_string($cell)) {
$isCli = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
$isCli = Env::isCli();
if ($isCli) {
$s = $cell;
} else {
Expand All @@ -267,4 +267,62 @@ public static function formatCell(mixed $cell, int $precision): string
}
return $s;
}

/**
* Format a 3D numeric tensor in a PyTorch-like multiline representation.
*
* @param array $tensor3d 3D array of ints/floats.
* @param int $headB Number of head 2D slices to display.
* @param int $tailB Number of tail 2D slices to display.
* @param int $headRows Number of head rows per 2D slice.
* @param int $tailRows Number of tail rows per 2D slice.
* @param int $headCols Number of head columns per 2D slice.
* @param int $tailCols Number of tail columns per 2D slice.
* @param string $label Prefix label used instead of "tensor".
* @param int $precision
* @return string
*/
public static function format3DTorch(array $tensor3d, int $headB = 5, int $tailB = 5, int $headRows = 5, int $tailRows = 5, int $headCols = 5, int $tailCols = 5, string $label = 'tensor', int $precision = 2): string
{
$B = count($tensor3d);
$idxs = [];
$useBEllipsis = false;
if ($B <= $headB + $tailB) {
for ($i = 0; $i < $B; $i++) {
$idxs[] = $i;
}
} else {
for ($i = 0; $i < $headB; $i++) {
$idxs[] = $i;
}
$useBEllipsis = true;
for ($i = $B - $tailB; $i < $B; $i++) {
$idxs[] = $i;
}
}

$blocks = [];
$format2d = function ($matrix) use ($headRows, $tailRows, $headCols, $tailCols, $precision) {
return self::format2DSummarized($matrix, $headRows, $tailRows, $headCols, $tailCols, $precision);
};

$limitHead = ($B <= $headB + $tailB) ? count($idxs) : $headB;
for ($i = 0; $i < $limitHead; $i++) {
$formatted2d = $format2d($tensor3d[$idxs[$i]]);
// Indent entire block by a single space efficiently
$blocks[] = ' ' . str_replace("\n", "\n ", $formatted2d);
}
if ($useBEllipsis) {
$blocks[] = ' ...';
}
if ($useBEllipsis) {
for ($i = $limitHead; $i < count($idxs); $i++) {
$formatted2d = $format2d($tensor3d[$idxs[$i]]);
$blocks[] = ' ' . str_replace("\n", "\n ", $formatted2d);
}
}

$joined = implode(",\n\n ", $blocks);
return $label . "([\n " . $joined . "\n])";
}
}
85 changes: 17 additions & 68 deletions src/PrettyPrint.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public function __invoke(...$args)
{
$end = PHP_EOL;
$start = '';
$sep = ' ';

// Named args for simple options
if (isset($args['end'])) {
Expand All @@ -78,6 +79,10 @@ public function __invoke(...$args)
$start = (string)$args['start'];
unset($args['start']);
}
if (isset($args['sep'])) {
$sep = (string)$args['sep'];
unset($args['sep']);
}

// Extract optional tensor formatting options from trailing options array
$fmt = [];
Expand All @@ -94,7 +99,7 @@ public function __invoke(...$args)
$last = end($args);
if (is_array($last)) {
$hasOptions = false;
$optionKeys = array_merge(['end', 'start'], $fmtKeys);
$optionKeys = array_merge(['end', 'start', 'sep'], $fmtKeys);
foreach ($optionKeys as $k) {
if (array_key_exists($k, $last)) {
$hasOptions = true;
Expand All @@ -108,6 +113,9 @@ public function __invoke(...$args)
if (array_key_exists('start', $last)) {
$start = (string)$last['start'];
}
if (array_key_exists('sep', $last)) {
$sep = (string)$last['sep'];
}
// Merge trailing array options (takes precedence over named if both provided)
$fmt = array_merge($fmt, $last);
array_pop($args);
Expand Down Expand Up @@ -135,7 +143,7 @@ public function __invoke(...$args)
}

// Auto-wrap with <pre> for web (non-CLI) usage
$isCli = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
$isCli = Env::isCli();
if (!$isCli) {
$start = '<pre>' . $start;
$end = $end . '</pre>';
Expand All @@ -160,15 +168,16 @@ public function __invoke(...$args)

// Label + single 3D tensor
if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && Validator::is3D($args[1])) {
$out = $this->format3DTorch(
$out = Formatter::format3DTorch(
$args[1],
(int)($fmt['headB'] ?? 5),
(int)($fmt['tailB'] ?? 5),
(int)($fmt['headRows'] ?? 5),
(int)($fmt['tailRows'] ?? 5),
(int)($fmt['headCols'] ?? 5),
(int)($fmt['tailCols'] ?? 5),
(string)($fmt['label'] ?? 'tensor')
(string)($fmt['label'] ?? 'tensor'),
$this->precision
);
echo $start . $out . $end;
$this->precision = $prevPrecision;
Expand Down Expand Up @@ -216,15 +225,16 @@ public function __invoke(...$args)
foreach ($args as $arg) {
if (is_array($arg)) {
if (Validator::is3D($arg)) {
$parts[] = $this->format3DTorch(
$parts[] = Formatter::format3DTorch(
$arg,
(int)($fmt['headB'] ?? 5),
(int)($fmt['tailB'] ?? 5),
(int)($fmt['headRows'] ?? 5),
(int)($fmt['tailRows'] ?? 5),
(int)($fmt['headCols'] ?? 5),
(int)($fmt['tailCols'] ?? 5),
(string)($fmt['label'] ?? 'tensor')
(string)($fmt['label'] ?? 'tensor'),
$this->precision
);
} elseif (Validator::is2D($arg)) {
$parts[] = Formatter::format2DTorch(
Expand All @@ -244,68 +254,7 @@ public function __invoke(...$args)
}
}

echo $start . implode(' ', $parts) . $end;
echo $start . implode($sep, $parts) . $end;
$this->precision = $prevPrecision;
}

// ---- Private helpers ----

/**
* Format a 3D numeric tensor in a PyTorch-like multiline representation.
*
* @param array $tensor3d 3D array of ints/floats.
* @param int $headB Number of head 2D slices to display.
* @param int $tailB Number of tail 2D slices to display.
* @param int $headRows Number of head rows per 2D slice.
* @param int $tailRows Number of tail rows per 2D slice.
* @param int $headCols Number of head columns per 2D slice.
* @param int $tailCols Number of tail columns per 2D slice.
* @param string $label Prefix label used instead of "tensor".
* @return string
*/
private function format3DTorch(array $tensor3d, int $headB = 5, int $tailB = 5, int $headRows = 5, int $tailRows = 5, int $headCols = 5, int $tailCols = 5, string $label = 'tensor'): string
{
$B = count($tensor3d);
$idxs = [];
$useBEllipsis = false;
if ($B <= $headB + $tailB) {
for ($i = 0; $i < $B; $i++) {
$idxs[] = $i;
}
} else {
for ($i = 0; $i < $headB; $i++) {
$idxs[] = $i;
}
$useBEllipsis = true;
for ($i = $B - $tailB; $i < $B; $i++) {
$idxs[] = $i;
}
}

$blocks = [];
$format2d = function ($matrix) use ($headRows, $tailRows, $headCols, $tailCols) {
return Formatter::format2DSummarized($matrix, $headRows, $tailRows, $headCols, $tailCols, $this->precision);
};

$limitHead = ($B <= $headB + $tailB) ? count($idxs) : $headB;
for ($i = 0; $i < $limitHead; $i++) {
$formatted2d = $format2d($tensor3d[$idxs[$i]]);
// Indent entire block by a single space efficiently
$blocks[] = ' ' . str_replace("\n", "\n ", $formatted2d);
}
if ($useBEllipsis) {
$blocks[] = ' ...';
}
if ($useBEllipsis) {
for ($i = $limitHead; $i < count($idxs); $i++) {
$formatted2d = $format2d($tensor3d[$idxs[$i]]);
$blocks[] = ' ' . str_replace("\n", "\n ", $formatted2d);
}
}

$joined = implode(",\n\n ", $blocks);
return $label . "([\n " . $joined . "\n])";
}
}

// 672/605/499/312==
5 changes: 5 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ function pp(...$args): void
function ppd(...$args): void
{
pprint(...$args);

if (PHP_SAPI === 'cli' && getenv('APP_ENV') === 'test') {
return;
}

exit;
}
}
Loading