pn app-id android|ios --json prints {"platform": ..., "app_id": ...} to
stdout. Plain-text output, exit codes, and stderr are unchanged without
the flag, which scripts/run-e2e.sh depends on.
A missing or invalid config still exits 1, with or without the flag.
That differs from pn devices --json, which exits 0 on an empty result,
because "no devices" is a valid answer to a query whereas an unreadable
config means there is no id to report. pn deps --json agrees. The
reasoning is in the docstring so it doesn't get "fixed" later.
_load_config_or_exit prints its error to stdout, which would poison the
JSON document. It now takes an optional stream, passed as stderr only
from app_id_command under --json; the other four callers are untouched.
pn deps --json has the same bug and is deliberately not fixed here --
the global change is defensible but breaks tests that assert config
errors on stdout, so it wants its own change.
Closes pythonnative#56
What
pn app-id android|ios --json prints {"platform": ..., "app_id": ...} to stdout. Plain-text
output, exit codes, and stderr are unchanged without the flag.
Closes #56.
The existing test was vacuous, and fixing it is half this PR
test_cli_app_id_resolves asserted the same string for both platforms, because a default
pn init scaffold sets only app.id with no per-platform override. Verified: main's test
passes against main's source with the platform branch inverted. The command's one branch had no
coverage at all.
It now uses a config with [android].application_id and [ios].bundle_id set so the values
diverge, and inverting the branch kills four tests. Worth reading as a bug found rather than a
test refactor. It also compares unstripped stdout byte-for-byte with stderr asserted empty, which
pins what scripts/run-e2e.sh:80 consumes — that line captures the output into a shell variable
and only checks it's non-empty, so an extra stdout line would corrupt APP_ID and surface much
later as a Maestro failure against a nonexistent bundle id.
Why the exit code differs from pn devices --json
#22 moved devices --json to exit 0 on an empty result because "no devices connected" is a valid
answer to a query. app-id has no empty case: either the config resolves, or it's missing or
invalid, which is a genuine failure with nothing to report. pn deps --json agrees and still
exits 1 when a target can't be satisfied.
Copying #22's exit-0 here would have been cargo-culting reasoning that was about emptiness, not
about --json. It's stated in the docstring so the next person doesn't "fix" it.
The stream change is scoped, and pn deps --json has the same bug
_load_config_or_exit prints its error to stdout, so any machine-readable mode built on it emits
plain text into the JSON document. It now takes an optional stream, passed as sys.stderr only
from app_id_command under --json. The other four callers — deps, run, build, logs —
are untouched and verified byte-identical.
pn deps --json still has this bug, on main and unchanged here: in a directory with no config
it exits 1 with Error: No pythonnative.toml found… on stdout, and json.load on that raises
JSONDecodeError. Deliberately not fixed in this PR. The follow-up is making the stderr behavior
global, which is defensible for all five callers since run, build, and logs already stream
progress to stdout — but it breaks existing tests that assert config errors on stdout, so it wants
its own change. test_cli_app_id_plain_error_stays_on_stdout pins the current scoping and will
fail loudly when someone takes that follow-up, which is the intent.
Compatibility promise: additive only
Keys may be added to the object; none will be removed or renamed. That's the stated reason the
feature exists — room to add the resolved app name later — and it's recorded in the docstring,
which renders into docs/api/cli.md, so it's a documented contract rather than a convention.
Candidates that fit without breaking anyone: the app name, display_name, python_version, and
which source the id came from (app.id versus a per-platform override).
Testing
Nine tests, all through run_pn so argparse wiring is exercised — #22 learned that the hard way,
where every in-process --json test passed with the add_argument deleted.
Five mutations, all killed: platform inversion (4 tests), the error routed back to stdout (1),
the --json add_argument deleted (5), the exit code changed to 0 (3), and indent=2 removed
(1). The formatting is pinned because it's part of what a script consumes and both existing
--json precedents use it.
The plain path was diffed against an actual main checkout byte-for-byte across both platforms
and the no-config case — stdout, stderr, and exit code identical in all six comparisons.
./scripts/check.sh passes. mkdocs build --strict passes from a clean site/.
Docs
docs/api/cli.md, mirroring the pn devices bullet's structure. building-for-release.md:218 is
left alone: it says the plain id is handy for downstream steps "without re-parsing the config,"
which is an argument for the plain output and still true. Adding --json there would mean
rewriting the sentence's point rather than inserting a clause.