functualize 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {functualize-0.2.0 → functualize-0.2.2}/CHANGELOG.md +112 -1
- {functualize-0.2.0 → functualize-0.2.2}/PKG-INFO +6 -1
- {functualize-0.2.0 → functualize-0.2.2}/README.md +5 -0
- {functualize-0.2.0 → functualize-0.2.2}/pyproject.toml +1 -1
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/SKILL.md +1 -1
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize-app/SKILL.md +1 -1
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize-cli/SKILL.md +1 -1
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize-skill/SKILL.md +1 -1
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/__init__.py +1 -1
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/main.py +6 -1
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/manifest.py +19 -1
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/package_ops.py +77 -18
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/plugin_cmd.py +4 -1
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/runtime.py +45 -2
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/self_cmd.py +102 -6
- functualize-0.2.2/src/functualize/_cli/self_update.py +365 -0
- {functualize-0.2.0 → functualize-0.2.2}/.gitignore +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/LICENSE +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/references/capabilities.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/references/config-and-secrets.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/references/discovery.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/references/environment.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/references/idiomatic.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/references/testing.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize/references/workflows.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize-app/references/standalone-scripts.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/skills/functualize-skill/references/distribution.md +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/__main__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_app/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_app/boot.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_app/decorators.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_app/environment.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_app/impl.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_app/models.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_app/state.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/annotation_utils.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/builtins.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/cursor_context.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/data.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/engine.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/flag_filtering.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/provenance.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/quote_handling.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/completions/shell_init.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/config.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/argument_history.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/config_snapshot_store.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/config_target.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/func_settings.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/invocation_preset.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/override_applicator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/path_suggestion.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/pending_execution.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/resolved_value_compat.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/settings_schema.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/shortcut_generator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/data/toml_writer.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/dispatch.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/info.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/inline_tui.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/introspect.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/orchestrator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/parallel_output.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/pep723.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/cli.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/context.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/generator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/registry.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/_shared/config.base.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/_shared/config.dev.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/_shared/config.prod.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/_shared/gitignore.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/config.base.ini.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/config.base.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-plugin/__init__.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-plugin/_plugin.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-plugin/pyproject.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-plugin/test_plugin.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/__init__.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/_errors.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/_events.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/_metadata.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/_protocols.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/_types.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/pyproject.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/domain-sdk/testing/__init__.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/file_plugin.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/full-interactivity/README.md.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/full-interactivity/__init__.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/full-interactivity/events_job.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/full-interactivity/interactive_job.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/full-interactivity/main.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/full-interactivity/pyproject.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/full-interactivity/workflow_job.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/job-folder/README.md.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/job-folder/config.base.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/job-folder/file_plugin.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/job-folder/sample_job.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/job.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/main.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/plugin-project/README.md.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/plugin-project/__init__.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/plugin-project/plugin.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/plugin-project/provider.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/plugin-project/pyproject.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/plugin-project/renderer.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/plugin.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/pyproject.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/screen.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/screen.tcss.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/simple/README.md.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/simple/__init__.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/simple/main.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/simple/pyproject.toml.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/simple/sample_job.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/scaffold/templates/standalone_job.py.j2 +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/skills.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/stdin_reader.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/app.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/bar.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/bar_items.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/breadcrumb_header_widget.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/chain_resolution.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/cli_arg_parser.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/config_diff.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/config_target_discovery.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/descriptor_fields.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/diff_view_widget.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/display_affinity.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/display_chrome.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/display_provider_discovery.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/display_slot.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/dynamic_footer.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/dynamic_footer_widget.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/dynamic_input_bar.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/editable_table.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/field_priority.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/focus.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/functualize_autocomplete.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/insert_mode.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/integration.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/job_execution.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/job_listing.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/key_handler.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/live_panel_widget.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/missing_args.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/modals/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/models/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/models/focus_state.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/models/panel_ring_controller.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/models/ring_models.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/models/source_chain.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/new_file_picker.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panel_host.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panel_live_zone.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panels/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panels/config_files.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panels/config_table.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panels/job_browser.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panels/ring.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/panels/settings_files.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/path_field_editor.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/path_suggestion_scanner.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/preflight_summary.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/settings_panel.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/settings_validator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/shell_mode.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/shortcut_save_modal.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/smart_bar_autocomplete.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/source_chain_detail.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/source_chain_providers.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/sync.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/theme_manager.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/thread_marshal.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_cli/tui/type_hint_formatter.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/_emit.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/chain.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/cli_adapter.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/errors.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/interactivity.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/job_config.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/manifest.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/merge.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/protocols.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/providers/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/providers/ini.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/providers/toml.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/registry.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/resolved_field.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/roles.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_config/sources.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/ast_extractor.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/cached_provider.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/filter_factory.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/group_options_extractor.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/hierarchy.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/hierarchy_errors.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/hierarchy_validator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/lazy_wrapper.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/naming.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/pipeline.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/pre_filter.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/providers.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/registry.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/schema_extractor.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/sync.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/transforms.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_discovery/version_resolver.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/ambient.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/invoke.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/job_context.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/live.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/log.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/perf.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/prompt.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/protocols.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/registry.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/runcontext.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/shell.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/sources.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/spec.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/state.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/state_store.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/stdin_collector.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/stdout.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/tty.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/workflow.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/capabilities/workflow_scope.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/context.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/errors.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/exec_policy.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/executor.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/explain.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/frontier.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/guards.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/job_graph.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/job_middleware.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/middleware.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/missing_value.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/preflight.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/resolution.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/result.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/scheduler.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/surface_routing.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/validation.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/workflow_runner.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/workflow_validation.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_engine/workflow_walker.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/_catalog_entries.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/_obs_types.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/_pattern_matcher.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/adapter.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/bus.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/catalog.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/hooks.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/instrumentation.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/introspection.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/middleware_stack.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/perf.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_events/tracing.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_gate/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_gate/_context.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_gate/_registry.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_gate/_resolver.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_gate/_strategy.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_gate/prompt_strategy.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_plugins/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_plugins/config.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_plugins/domain_metadata.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_plugins/domain_registry.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_plugins/loader.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/agent_epilog.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/cache_format.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/capability_names.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/config_class_detection.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/di.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/display_detection.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/entry_points.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/fingerprint.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/graph.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/group_options_detection.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/job_filter.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/job_schema.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/lazy.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/locator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/middleware.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/modules.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/pre_filter.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/resilient.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/resolution.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/state_format.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/state_store.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_primitives/stdout_emitter.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/annotations.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/commands.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/descriptors.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/enums.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/errors.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/exit_codes.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/from_job.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/group_options.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/input_modes.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/interactivity.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/job_declaration.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/naming.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/protocols.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/redaction.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/settings.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/shell.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/stdout.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/_types/workflow.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/_workflow_resume.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/adapters/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/adapters/_validation.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/adapters/cli.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/adapters/click_params.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/adapters/lazy_command.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/adapters/surface_gate.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/adapters/tui.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/commands.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/config.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/core.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/fallback.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/presets.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/app/utils.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_invoke.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_job_context.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_log.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_middleware.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_perf.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_prompt.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_protocols.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_runcontext.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_shell.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_sources.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_state.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_state_store.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_stdout.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/_workflow_scope.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/capabilities.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/context.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/decorators.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/job/markers.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/plugin/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/plugin/protocols.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/py.typed +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/testing/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/testing/builder.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/testing/doubles.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/testing/shell.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/testing/stdout.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/types/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/_prompt_modal.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/display.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/fullscreen/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/fullscreen/app.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/fullscreen/screens.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/fullscreen/widgets.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/py.typed +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/stdout_surface.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/ui/textual_app.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/workflow/__init__.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/workflow/_decorator.py +0 -0
- {functualize-0.2.0 → functualize-0.2.2}/src/functualize/workflow/_validation.py +0 -0
|
@@ -7,6 +7,115 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.2] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added — the standalone binary manages itself
|
|
13
|
+
|
|
14
|
+
`func builtin self update` on a standalone install now **replaces the binary**.
|
|
15
|
+
It reads the release source baked into the distribution, fetches that release,
|
|
16
|
+
verifies the archive against its `SHA256SUMS` *before unpacking*, and swaps the
|
|
17
|
+
file through a staged `os.replace` in the target's own directory — atomic, so
|
|
18
|
+
an interrupted update leaves one working binary or the other, never a truncated
|
|
19
|
+
file. An archive the checksums file does not mention is refused exactly like one
|
|
20
|
+
that fails to match.
|
|
21
|
+
|
|
22
|
+
`self install`, `self uninstall`, `plugin install` and `plugin uninstall` work
|
|
23
|
+
on a standalone install too, through the bundled interpreter's own pip. A binary
|
|
24
|
+
is the install method for a machine with no Python toolchain, so requiring `uv`
|
|
25
|
+
on `PATH` to add a dependency would defeat it — and the baked distribution ships
|
|
26
|
+
no uv. `self python -- ...` remains the escape hatch; `self uv` is the one
|
|
27
|
+
command a standalone install does not have.
|
|
28
|
+
|
|
29
|
+
The release source is baked, never hard-coded. An application that builds its
|
|
30
|
+
own PyApp binary over functualize writes its own `standalone-release.json` and
|
|
31
|
+
points its users at its own releases; its absence refuses cleanly rather than
|
|
32
|
+
offering functualize's artifact to somebody else's users.
|
|
33
|
+
|
|
34
|
+
### Fixed — the binaries 0.2.1 announced and could not build
|
|
35
|
+
|
|
36
|
+
**All seven `binaries` jobs failed on 0.2.1.** The reported error was PyApp
|
|
37
|
+
panicking without `PYAPP_DISTRIBUTION_PYTHON_PATH`, but fixing that alone was
|
|
38
|
+
not enough: building a binary locally showed the baked artifact was unusable in
|
|
39
|
+
a second, quieter way.
|
|
40
|
+
|
|
41
|
+
- **A virtual environment is not a distribution.** The bake produced
|
|
42
|
+
`uv venv --relocatable`, whose `bin/python` is a symlink to the interpreter it
|
|
43
|
+
was created from and whose standard library lives in that interpreter's
|
|
44
|
+
prefix. Unpacked on another machine it contains no Python at all, and the
|
|
45
|
+
binary died on launch with `project execution failed / No such file or
|
|
46
|
+
directory (os error 2)`. The bake now copies the python-build-standalone
|
|
47
|
+
installation itself.
|
|
48
|
+
|
|
49
|
+
- **uv installs a workspace root as editable by default**, writing a `.pth`
|
|
50
|
+
that points at the build machine and leaving no package in site-packages — a
|
|
51
|
+
binary that starts and then imports nothing. The bake passes `--no-editable`
|
|
52
|
+
and asserts no editable `.pth` survives.
|
|
53
|
+
|
|
54
|
+
- **`uv python install` offers only the host libc's distributions**, so a musl
|
|
55
|
+
target baked on a glibc runner would embed a glibc interpreter. The musl bakes
|
|
56
|
+
run inside Alpine, with `build-base` installed because aarch64-musl has no
|
|
57
|
+
published wheel for one dependency.
|
|
58
|
+
|
|
59
|
+
- **The size-measurement step failed six working builds.** `ls a b` exits
|
|
60
|
+
non-zero when one of the two names is absent, and under `set -o pipefail`
|
|
61
|
+
that failed the job *after* a successful ten-minute build.
|
|
62
|
+
|
|
63
|
+
- **A `python.exe` at the distribution root**, not under `Scripts/`, is the
|
|
64
|
+
Windows layout for a standalone install.
|
|
65
|
+
|
|
66
|
+
### Fixed — a standalone install could not be identified
|
|
67
|
+
|
|
68
|
+
PyApp launches the application as `python -c "..."`, so `argv[0]` is the literal
|
|
69
|
+
string `-c`. Self-management reverse-maps that to a distribution, found nothing,
|
|
70
|
+
and read a healthy binary as a degraded install: every mutating command refused
|
|
71
|
+
with exit `3`, `self doctor` reported "self-management is unavailable here", and
|
|
72
|
+
the installation registry recorded `<prefix>/bin/-c` — a path that never existed
|
|
73
|
+
— which it then correctly reported as stale on the run that created it.
|
|
74
|
+
|
|
75
|
+
Identity now comes from PyApp itself, which supplies the executable's absolute
|
|
76
|
+
path in `PYAPP` when built with `PYAPP_PASS_LOCATION=1`. A standalone install is
|
|
77
|
+
no longer degraded for want of an owning distribution: it has none by
|
|
78
|
+
construction, because it is a file rather than a package.
|
|
79
|
+
|
|
80
|
+
`self update` previously delegated to `func pyapp update`. That command does not
|
|
81
|
+
work on a pre-baked binary: PyApp hides it unless `PYAPP_EXPOSE_UPDATE=1`,
|
|
82
|
+
refuses it outright under `PYAPP_SKIP_INSTALL=1`, and would `pip install
|
|
83
|
+
--upgrade` from an index if it ran — dissolving the offline-complete
|
|
84
|
+
distribution the binary exists to be.
|
|
85
|
+
|
|
86
|
+
`install.sh` and `install.ps1` work from **this** release onward; 0.2.1's note
|
|
87
|
+
to that effect was premature, as it shipped no binaries.
|
|
88
|
+
|
|
89
|
+
## [0.2.1] - 2026-09-04
|
|
90
|
+
|
|
91
|
+
### Fixed — the release pipeline that could not deliver 0.2.0's binaries
|
|
92
|
+
|
|
93
|
+
**No library changes.** This release exists to ship the standalone binaries that
|
|
94
|
+
0.2.0 announced and could not produce. If you install from PyPI, 0.2.1 and 0.2.0
|
|
95
|
+
are the same code.
|
|
96
|
+
|
|
97
|
+
The binary pipeline shipped in 0.2.0 and ran for the first time on its tag —
|
|
98
|
+
these stages cannot execute before a tag exists, so the tag *was* the first test.
|
|
99
|
+
Three defects surfaced:
|
|
100
|
+
|
|
101
|
+
- **The `macos-13` runner label was retired on 2025-12-04.** A workflow naming a
|
|
102
|
+
dead label does not fail — it queues forever. The release sat "in progress"
|
|
103
|
+
for an hour with no runner ever assigned. Now `macos-15-intel`, which is the
|
|
104
|
+
last x86_64 macOS image GitHub will offer.
|
|
105
|
+
|
|
106
|
+
- **The binary and checksum stages required their entire upstream matrix to
|
|
107
|
+
succeed.** `needs.<job>.result` is the aggregate across a matrix, so one
|
|
108
|
+
target failing skipped all seven — `fail-fast: false` buys nothing across a
|
|
109
|
+
stage boundary. A broken platform now costs its own binary and nothing else.
|
|
110
|
+
|
|
111
|
+
- **The GitHub Release depended on the binaries.** A failed binary build
|
|
112
|
+
therefore skipped the release entirely, leaving PyPI with the version and
|
|
113
|
+
GitHub with a bare tag. It is now gated on the PyPI publish alone; the wheel
|
|
114
|
+
is the primary artifact and the binaries ride along.
|
|
115
|
+
|
|
116
|
+
`install.sh` and `install.ps1` work from this release onward. 0.2.0 remains a
|
|
117
|
+
PyPI-only release.
|
|
118
|
+
|
|
10
119
|
## [0.2.0] - 2026-09-04
|
|
11
120
|
|
|
12
121
|
### Added — a standalone binary, and installations that manage themselves
|
|
@@ -1065,7 +1174,9 @@ function knowing which.
|
|
|
1065
1174
|
through the injected `Log` is deferred to a later release.
|
|
1066
1175
|
*(Fixed in 0.1.1.)*
|
|
1067
1176
|
|
|
1068
|
-
[Unreleased]: https://github.com/raicing-ai/functualize/compare/v0.2.
|
|
1177
|
+
[Unreleased]: https://github.com/raicing-ai/functualize/compare/v0.2.2...HEAD
|
|
1178
|
+
[0.2.2]: https://github.com/raicing-ai/functualize/compare/v0.2.1...v0.2.2
|
|
1179
|
+
[0.2.1]: https://github.com/raicing-ai/functualize/compare/v0.2.0...v0.2.1
|
|
1069
1180
|
[0.2.0]: https://github.com/raicing-ai/functualize/compare/v0.1.3...v0.2.0
|
|
1070
1181
|
[0.1.3]: https://github.com/raicing-ai/functualize/compare/v0.1.2...v0.1.3
|
|
1071
1182
|
[0.1.2]: https://github.com/raicing-ai/functualize/compare/v0.1.1...v0.1.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: functualize
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A reusable Python CLI framework with auto-discovery, structured run context, layered configuration, and TUI integration
|
|
5
5
|
Project-URL: Homepage, https://github.com/raicing-ai/functualize
|
|
6
6
|
Project-URL: Repository, https://github.com/raicing-ai/functualize
|
|
@@ -156,6 +156,11 @@ self update` then restores anything you had added — including packages install
|
|
|
156
156
|
> binary, a `uv tool` install, a `pipx` install, or a project checkout. A bare `pip install`
|
|
157
157
|
> into a system interpreter is not self-managing: the command prints guidance, changes
|
|
158
158
|
> nothing, and exits `3`. `func builtin self doctor` tells you which kind you have.
|
|
159
|
+
>
|
|
160
|
+
> The standalone binary updates by **replacing itself**: it fetches the release for its own
|
|
161
|
+
> platform, verifies it against the release's `SHA256SUMS` before unpacking, and swaps the
|
|
162
|
+
> file. Packages you had added are reinstalled into the new distribution afterwards — a new
|
|
163
|
+
> binary unpacks a new environment, so they do not carry over on their own.
|
|
159
164
|
|
|
160
165
|
### How `func` finds jobs
|
|
161
166
|
|
|
@@ -107,6 +107,11 @@ self update` then restores anything you had added — including packages install
|
|
|
107
107
|
> binary, a `uv tool` install, a `pipx` install, or a project checkout. A bare `pip install`
|
|
108
108
|
> into a system interpreter is not self-managing: the command prints guidance, changes
|
|
109
109
|
> nothing, and exits `3`. `func builtin self doctor` tells you which kind you have.
|
|
110
|
+
>
|
|
111
|
+
> The standalone binary updates by **replacing itself**: it fetches the release for its own
|
|
112
|
+
> platform, verifies it against the release's `SHA256SUMS` before unpacking, and swaps the
|
|
113
|
+
> file. Packages you had added are reinstalled into the new distribution afterwards — a new
|
|
114
|
+
> binary unpacks a new environment, so they do not carry over on their own.
|
|
110
115
|
|
|
111
116
|
### How `func` finds jobs
|
|
112
117
|
|
|
@@ -1758,7 +1758,12 @@ def _register_this_installation() -> None:
|
|
|
1758
1758
|
# than imported so the warm path stays free of that module's dataclass
|
|
1759
1759
|
# codegen. `tests/_cli/test_manifest.py` asserts the two agree.
|
|
1760
1760
|
argv0 = sys.argv[0] if sys.argv else ""
|
|
1761
|
-
|
|
1761
|
+
pyapp = os.environ.get("PYAPP", "")
|
|
1762
|
+
if pyapp and pyapp != "1":
|
|
1763
|
+
# A standalone binary: `argv0` is `-c` and useless. See
|
|
1764
|
+
# `resolve_binary_path`.
|
|
1765
|
+
binary_path = str(Path(pyapp).resolve())
|
|
1766
|
+
elif not argv0:
|
|
1762
1767
|
binary_path = ""
|
|
1763
1768
|
elif "/" in argv0 or "\\" in argv0:
|
|
1764
1769
|
binary_path = str(Path(argv0).resolve())
|
|
@@ -164,9 +164,18 @@ class Manifest:
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
|
|
167
|
-
def resolve_binary_path(
|
|
167
|
+
def resolve_binary_path(
|
|
168
|
+
argv0: str, executable: str, standalone_binary: str | None = None
|
|
169
|
+
) -> str:
|
|
168
170
|
"""A stable identity for "this installation", whatever `argv[0]` looks like.
|
|
169
171
|
|
|
172
|
+
`standalone_binary` wins outright when supplied. PyApp launches a standalone
|
|
173
|
+
binary as `python -c "..."`, so `argv[0]` is the literal string `-c` and
|
|
174
|
+
every derivation below produces `<prefix>/bin/-c` -- a path that has never
|
|
175
|
+
existed, which the registry then correctly reports as stale on the very run
|
|
176
|
+
that recorded it. PyApp hands over the real executable path in `PYAPP`
|
|
177
|
+
(built with `PYAPP_PASS_LOCATION=1`), and that is the only honest answer.
|
|
178
|
+
|
|
170
179
|
`sys.argv[0]` is not stable across invocation styles: a direct call gives an
|
|
171
180
|
absolute path, while `uv run func` gives the bare name `func`. Recording it
|
|
172
181
|
raw registers one installation twice, and then reports the bare-name copy as
|
|
@@ -181,6 +190,15 @@ def resolve_binary_path(argv0: str, executable: str) -> str:
|
|
|
181
190
|
avoid importing this module; `tests/_cli/test_manifest.py` asserts the two
|
|
182
191
|
agree.
|
|
183
192
|
"""
|
|
193
|
+
if standalone_binary:
|
|
194
|
+
# Substituted for `argv0` rather than returned directly, so it goes
|
|
195
|
+
# through the same normalization. PyApp's `PYAPP` is always absolute and
|
|
196
|
+
# falls out of the separator branch unchanged; a bare name -- which is
|
|
197
|
+
# what the `FUNCTUALIZE_RUNTIME=standalone` override sees -- must still
|
|
198
|
+
# resolve against the interpreter's directory and not the working one,
|
|
199
|
+
# or this disagrees with `main.py`'s warm path and the registry records
|
|
200
|
+
# the same installation twice.
|
|
201
|
+
argv0 = standalone_binary
|
|
184
202
|
if not argv0:
|
|
185
203
|
return ""
|
|
186
204
|
if "/" in argv0 or "\\" in argv0:
|
|
@@ -38,11 +38,12 @@ from functualize._cli.runtime import Detection, InstallMode
|
|
|
38
38
|
from functualize.app.utils import ExitCode
|
|
39
39
|
|
|
40
40
|
if TYPE_CHECKING:
|
|
41
|
-
from collections.abc import Callable, Iterable, Sequence
|
|
41
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
42
42
|
|
|
43
43
|
__all__ = [
|
|
44
44
|
"LossyReceiptError",
|
|
45
45
|
"MissingToolError",
|
|
46
|
+
"StandaloneUpdateError",
|
|
46
47
|
"Requirement",
|
|
47
48
|
"announce",
|
|
48
49
|
"capture_environment",
|
|
@@ -510,6 +511,24 @@ def owned_python() -> str:
|
|
|
510
511
|
# ---------------------------------------------------------------------------
|
|
511
512
|
|
|
512
513
|
|
|
514
|
+
class StandaloneUpdateError(Exception):
|
|
515
|
+
"""``self update`` on a standalone install is not a subprocess.
|
|
516
|
+
|
|
517
|
+
Every other mode delegates to a package manager, so its update is a command
|
|
518
|
+
tuple. A standalone binary is a single file, and updating it means
|
|
519
|
+
downloading a release and replacing that file -- work that happens
|
|
520
|
+
in-process, in :mod:`functualize._cli.self_update`. Raised rather than
|
|
521
|
+
returned so a caller that forgets to handle it fails loudly instead of
|
|
522
|
+
running an empty command list and reporting success.
|
|
523
|
+
|
|
524
|
+
PyApp's own updater is not an option: it is hidden unless
|
|
525
|
+
``PYAPP_EXPOSE_UPDATE=1``, refuses outright under ``PYAPP_SKIP_INSTALL=1``
|
|
526
|
+
(`"Cannot update as installation is disabled"`), and would ``pip install
|
|
527
|
+
--upgrade`` from an index if it ran -- replacing the offline-complete
|
|
528
|
+
environment the binary exists to be.
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
|
|
513
532
|
def update_commands(
|
|
514
533
|
detection: Detection, binary_path: str
|
|
515
534
|
) -> tuple[tuple[str, ...], ...]:
|
|
@@ -526,15 +545,23 @@ def update_commands(
|
|
|
526
545
|
backstop that keeps a refusal from silently becoming a command.
|
|
527
546
|
MissingToolError: the mode's manager is not installed.
|
|
528
547
|
"""
|
|
548
|
+
# Standalone is checked *before* the owner guard. It has no owning
|
|
549
|
+
# distribution by construction -- it is a file, not a package -- so the
|
|
550
|
+
# guard that protects every other mode from acting on a name it could not
|
|
551
|
+
# resolve would refuse every healthy binary.
|
|
552
|
+
if detection.mode is InstallMode.STANDALONE:
|
|
553
|
+
if detection.standalone_binary is None:
|
|
554
|
+
raise ValueError(
|
|
555
|
+
"standalone installations cannot be updated without knowing "
|
|
556
|
+
"their own path"
|
|
557
|
+
)
|
|
558
|
+
raise StandaloneUpdateError(detection.standalone_binary)
|
|
559
|
+
|
|
529
560
|
distribution = detection.owning_distribution
|
|
530
561
|
if detection.degraded or distribution is None:
|
|
531
562
|
raise ValueError(f"{detection.mode.value} installations are not self-managing")
|
|
532
563
|
|
|
533
564
|
match detection.mode:
|
|
534
|
-
case InstallMode.STANDALONE:
|
|
535
|
-
# PyApp's own updater, reachable at the renamed self-command
|
|
536
|
-
# (`PYAPP_SELF_COMMAND=pyapp`). It replaces the binary in place.
|
|
537
|
-
return ((binary_path, "pyapp", "update"),)
|
|
538
565
|
case InstallMode.TOOL_UV:
|
|
539
566
|
return ((resolve_uv(), "tool", "upgrade", distribution),)
|
|
540
567
|
case InstallMode.TOOL_PIPX:
|
|
@@ -562,18 +589,14 @@ def install_commands(detection: Detection, package: str) -> tuple[tuple[str, ...
|
|
|
562
589
|
MissingToolError: the mode's manager is not installed.
|
|
563
590
|
LossyReceiptError: a uv receipt could not be reproduced faithfully.
|
|
564
591
|
"""
|
|
592
|
+
if detection.mode is InstallMode.STANDALONE:
|
|
593
|
+
return ((*_bundled_pip(), "install", package),)
|
|
594
|
+
|
|
565
595
|
distribution = detection.owning_distribution
|
|
566
596
|
if detection.degraded or distribution is None:
|
|
567
597
|
raise ValueError(f"{detection.mode.value} installations are not self-managing")
|
|
568
598
|
|
|
569
599
|
match detection.mode:
|
|
570
|
-
case InstallMode.STANDALONE:
|
|
571
|
-
# `--python` targets the bundled interpreter explicitly. Without it
|
|
572
|
-
# uv picks a Python by its own discovery rules, which in a shell
|
|
573
|
-
# with an activated venv is somebody else's environment.
|
|
574
|
-
return (
|
|
575
|
-
(resolve_uv(), "pip", "install", "--python", owned_python(), package),
|
|
576
|
-
)
|
|
577
600
|
case InstallMode.TOOL_UV:
|
|
578
601
|
uv = resolve_uv()
|
|
579
602
|
receipt = read_receipt(Path(sys.prefix))
|
|
@@ -597,15 +620,14 @@ def uninstall_commands(
|
|
|
597
620
|
MissingToolError: the mode's manager is not installed.
|
|
598
621
|
LossyReceiptError: a uv receipt could not be reproduced faithfully.
|
|
599
622
|
"""
|
|
623
|
+
if detection.mode is InstallMode.STANDALONE:
|
|
624
|
+
return ((*_bundled_pip(), "uninstall", "-y", package),)
|
|
625
|
+
|
|
600
626
|
distribution = detection.owning_distribution
|
|
601
627
|
if detection.degraded or distribution is None:
|
|
602
628
|
raise ValueError(f"{detection.mode.value} installations are not self-managing")
|
|
603
629
|
|
|
604
630
|
match detection.mode:
|
|
605
|
-
case InstallMode.STANDALONE:
|
|
606
|
-
return (
|
|
607
|
-
(resolve_uv(), "pip", "uninstall", "--python", owned_python(), package),
|
|
608
|
-
)
|
|
609
631
|
case InstallMode.TOOL_UV:
|
|
610
632
|
uv = resolve_uv()
|
|
611
633
|
receipt = read_receipt(Path(sys.prefix))
|
|
@@ -638,16 +660,53 @@ def render(argv: Sequence[str]) -> str:
|
|
|
638
660
|
return " ".join(parts)
|
|
639
661
|
|
|
640
662
|
|
|
641
|
-
def
|
|
663
|
+
def _bundled_pip() -> tuple[str, ...]:
|
|
664
|
+
"""The bundled interpreter's own pip, as a command prefix.
|
|
665
|
+
|
|
666
|
+
Not uv. A standalone binary is the install method for a machine with no
|
|
667
|
+
Python toolchain, so requiring ``uv`` on ``PATH`` before a package can be
|
|
668
|
+
added defeats the reason it exists -- and PyApp's baked distribution ships
|
|
669
|
+
no uv, so ``resolve_uv()`` would raise on the very install method that
|
|
670
|
+
cannot fix it. pip is present in the distribution and the bake asserts so.
|
|
671
|
+
|
|
672
|
+
``-m pip`` rather than the ``pip`` script: the script's shebang is written
|
|
673
|
+
at build time and points at the *build machine's* path, which is not where
|
|
674
|
+
the distribution unpacks.
|
|
675
|
+
"""
|
|
676
|
+
return (owned_python(), "-m", "pip")
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def script_name(environ: Mapping[str, str] | None = None) -> str:
|
|
642
680
|
"""The console script the user actually typed, for use in guidance.
|
|
643
681
|
|
|
644
682
|
Never a hard-coded ``func``: in a consumer application this text has to
|
|
645
683
|
name *that* application's script or it tells the user to run a command they
|
|
646
684
|
do not have.
|
|
685
|
+
|
|
686
|
+
And never ``-c``. PyApp launches a standalone binary as ``python -c "..."``,
|
|
687
|
+
so ``argv[0]`` is that literal string and guidance built from it reads
|
|
688
|
+
``Run `-c builtin self doctor` `` -- a command nobody can type. PyApp
|
|
689
|
+
supplies the real name in ``PYAPP_COMMAND_NAME``; falling back to the
|
|
690
|
+
binary's own basename covers a build that did not expose one.
|
|
647
691
|
"""
|
|
692
|
+
env = os.environ if environ is None else environ
|
|
693
|
+
|
|
694
|
+
exposed = env.get("PYAPP_COMMAND_NAME", "")
|
|
695
|
+
if exposed:
|
|
696
|
+
return exposed
|
|
697
|
+
|
|
648
698
|
argv0 = sys.argv[0] if sys.argv else ""
|
|
649
699
|
name = argv0.replace("\\", "/").rsplit("/", 1)[-1]
|
|
650
|
-
|
|
700
|
+
if name and name != "-c":
|
|
701
|
+
return name
|
|
702
|
+
|
|
703
|
+
binary = env.get("PYAPP", "")
|
|
704
|
+
if binary and binary != "1":
|
|
705
|
+
basename = binary.replace("\\", "/").rsplit("/", 1)[-1]
|
|
706
|
+
if basename:
|
|
707
|
+
return basename
|
|
708
|
+
|
|
709
|
+
return "func"
|
|
651
710
|
|
|
652
711
|
|
|
653
712
|
def refuse(detection: Detection, what: str) -> NoReturn:
|
|
@@ -174,10 +174,13 @@ def _binary_and_config() -> tuple[str, Path]:
|
|
|
174
174
|
import sys
|
|
175
175
|
|
|
176
176
|
from functualize._cli import manifest
|
|
177
|
+
from functualize._cli.runtime import detect_from_process
|
|
177
178
|
from functualize.app.utils import resolve_user_config_dir
|
|
178
179
|
|
|
179
180
|
binary = manifest.resolve_binary_path(
|
|
180
|
-
sys.argv[0] if sys.argv else "",
|
|
181
|
+
sys.argv[0] if sys.argv else "",
|
|
182
|
+
sys.executable,
|
|
183
|
+
detect_from_process().standalone_binary,
|
|
181
184
|
)
|
|
182
185
|
return binary, resolve_user_config_dir()
|
|
183
186
|
|
|
@@ -90,10 +90,23 @@ class Detection:
|
|
|
90
90
|
#: wrong-owner failure this module exists to prevent, so ``None`` forces
|
|
91
91
|
#: the refusal path.
|
|
92
92
|
owning_distribution: str | None
|
|
93
|
+
#: The standalone binary's own absolute path, as PyApp reports it in
|
|
94
|
+
#: ``PYAPP`` when built with ``PYAPP_PASS_LOCATION=1``. ``None`` for every
|
|
95
|
+
#: other mode, and for a standalone binary built without that flag or whose
|
|
96
|
+
#: ``current_exe()`` lookup failed.
|
|
97
|
+
standalone_binary: str | None = None
|
|
93
98
|
|
|
94
99
|
@property
|
|
95
100
|
def degraded(self) -> bool:
|
|
96
101
|
"""Either the environment or the owner is unusable for management."""
|
|
102
|
+
if self.mode is InstallMode.STANDALONE:
|
|
103
|
+
# A standalone binary has no owning distribution *by construction*
|
|
104
|
+
# -- it is a file, not a package -- so the absence that degrades
|
|
105
|
+
# every other mode says nothing here. What a mutating command needs
|
|
106
|
+
# is the executable to act on, and PyApp launches the application
|
|
107
|
+
# through `python -c`, so `argv[0]` is the literal string `-c` and
|
|
108
|
+
# can never supply it. The binary's own path is the discriminator.
|
|
109
|
+
return self.standalone_binary is None
|
|
97
110
|
return self.mode.degraded or self.owning_distribution is None
|
|
98
111
|
|
|
99
112
|
|
|
@@ -233,9 +246,33 @@ def detect(
|
|
|
233
246
|
return Detection(
|
|
234
247
|
mode=_detect_mode(prefix, base_prefix, environ, cwd),
|
|
235
248
|
owning_distribution=_owning_distribution(argv0),
|
|
249
|
+
standalone_binary=_standalone_binary(environ, argv0),
|
|
236
250
|
)
|
|
237
251
|
|
|
238
252
|
|
|
253
|
+
def _standalone_binary(environ: Mapping[str, str], argv0: str) -> str | None:
|
|
254
|
+
"""The running executable's path, for a standalone installation.
|
|
255
|
+
|
|
256
|
+
``PYAPP`` holds the absolute path only when the binary was built with
|
|
257
|
+
``PYAPP_PASS_LOCATION=1``; older builds set the literal ``"1"`` and a build
|
|
258
|
+
whose ``current_exe()`` failed sets the empty string. Both are "unknown",
|
|
259
|
+
not a path — returning ``"1"`` here would have every mutating command
|
|
260
|
+
operate on a file named ``1`` in the working directory.
|
|
261
|
+
|
|
262
|
+
``argv0`` is the second rung, and it is not a fallback guess. Inside a real
|
|
263
|
+
PyApp binary it is the literal string ``-c`` and this rung cannot fire. It
|
|
264
|
+
fires only when the mode was pinned with ``FUNCTUALIZE_RUNTIME=standalone``
|
|
265
|
+
— where ``argv0`` genuinely *is* the command that ran — which is what keeps
|
|
266
|
+
that override useful rather than a way to manufacture a degraded install.
|
|
267
|
+
"""
|
|
268
|
+
value = environ.get("PYAPP", "")
|
|
269
|
+
if value and value != "1":
|
|
270
|
+
return value
|
|
271
|
+
if argv0 and argv0 != "-c":
|
|
272
|
+
return argv0
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
239
276
|
def _detect_mode(
|
|
240
277
|
prefix: str,
|
|
241
278
|
base_prefix: str,
|
|
@@ -254,8 +291,14 @@ def _detect_mode(
|
|
|
254
291
|
f"Expected one of: {', '.join(m.value for m in InstallMode)}"
|
|
255
292
|
) from None
|
|
256
293
|
|
|
257
|
-
# 2. The binary announces itself: PyApp injects PYAPP
|
|
258
|
-
|
|
294
|
+
# 2. The binary announces itself: PyApp injects PYAPP into its runtime --
|
|
295
|
+
# the executable's own path under `PYAPP_PASS_LOCATION=1`, otherwise
|
|
296
|
+
# "1". Membership, not truthiness: PyApp sets it to the *empty string*
|
|
297
|
+
# when `current_exe()` fails (`distribution.rs:56`), and a binary that
|
|
298
|
+
# cannot name itself is still a standalone binary. Reading that as
|
|
299
|
+
# "not standalone" would send it down the project or unknown rungs and
|
|
300
|
+
# describe it as something it is not.
|
|
301
|
+
if "PYAPP" in environ or environ.get("PYAPP_COMMAND_NAME"):
|
|
259
302
|
return InstallMode.STANDALONE
|
|
260
303
|
|
|
261
304
|
prefix_path = Path(prefix)
|
|
@@ -39,7 +39,7 @@ from pathlib import Path
|
|
|
39
39
|
|
|
40
40
|
import click
|
|
41
41
|
|
|
42
|
-
from functualize._cli.runtime import Detection, detect_from_process
|
|
42
|
+
from functualize._cli.runtime import Detection, InstallMode, detect_from_process
|
|
43
43
|
from functualize.app.utils import ExitCode
|
|
44
44
|
|
|
45
45
|
# `manifest` and `package_ops` are imported *inside* the commands that need
|
|
@@ -154,7 +154,37 @@ def _check_install(detection: Detection) -> list[Check]:
|
|
|
154
154
|
checks = [
|
|
155
155
|
Check("install-mode", CheckStatus.INFO, detection.mode.value),
|
|
156
156
|
]
|
|
157
|
-
if detection.
|
|
157
|
+
if detection.mode is InstallMode.STANDALONE:
|
|
158
|
+
# Not a warning, and not "unknown". A standalone binary has no owning
|
|
159
|
+
# distribution *by construction* -- it is a file, not a package -- so
|
|
160
|
+
# reporting the absence as a fault would tell a healthy installation
|
|
161
|
+
# that self-management is unavailable, which it is not. What identifies
|
|
162
|
+
# it is the executable, and that is worth showing.
|
|
163
|
+
checks.append(
|
|
164
|
+
Check(
|
|
165
|
+
"owning-distribution",
|
|
166
|
+
CheckStatus.INFO,
|
|
167
|
+
"not applicable — a standalone binary manages itself",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
if detection.standalone_binary is None:
|
|
171
|
+
checks.append(
|
|
172
|
+
Check(
|
|
173
|
+
"binary-path",
|
|
174
|
+
CheckStatus.WARNING,
|
|
175
|
+
"this binary cannot determine its own location",
|
|
176
|
+
remedy=(
|
|
177
|
+
"It was built without PYAPP_PASS_LOCATION=1, so there is "
|
|
178
|
+
"nothing for `self update` to replace. Reinstall from a "
|
|
179
|
+
"current release."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
checks.append(
|
|
185
|
+
Check("binary-path", CheckStatus.OK, detection.standalone_binary)
|
|
186
|
+
)
|
|
187
|
+
elif detection.owning_distribution is None:
|
|
158
188
|
checks.append(
|
|
159
189
|
Check(
|
|
160
190
|
"owning-distribution",
|
|
@@ -470,7 +500,11 @@ def doctor(output_format: str) -> None:
|
|
|
470
500
|
def _this_binary() -> str:
|
|
471
501
|
from functualize._cli import manifest
|
|
472
502
|
|
|
473
|
-
return manifest.resolve_binary_path(
|
|
503
|
+
return manifest.resolve_binary_path(
|
|
504
|
+
sys.argv[0] if sys.argv else "",
|
|
505
|
+
sys.executable,
|
|
506
|
+
detect_from_process().standalone_binary,
|
|
507
|
+
)
|
|
474
508
|
|
|
475
509
|
|
|
476
510
|
def _config_dir() -> Path:
|
|
@@ -496,9 +530,18 @@ def update(assume_yes: bool) -> None:
|
|
|
496
530
|
package_ops.refuse(detection, "update")
|
|
497
531
|
|
|
498
532
|
binary = _this_binary()
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
533
|
+
try:
|
|
534
|
+
commands = package_ops.plan_or_exit(
|
|
535
|
+
lambda: package_ops.update_commands(detection, binary)
|
|
536
|
+
)
|
|
537
|
+
except package_ops.StandaloneUpdateError:
|
|
538
|
+
# A standalone binary has no package manager to delegate to: updating
|
|
539
|
+
# it means fetching a release and replacing one file, which happens
|
|
540
|
+
# in-process. Raised rather than returned as an empty command list, so
|
|
541
|
+
# forgetting this branch fails loudly instead of reporting success
|
|
542
|
+
# having done nothing.
|
|
543
|
+
raise SystemExit(_update_standalone(binary, assume_yes)) from None
|
|
544
|
+
|
|
502
545
|
package_ops.announce(commands, assume_yes)
|
|
503
546
|
|
|
504
547
|
config_dir = _config_dir()
|
|
@@ -529,6 +572,59 @@ def update(assume_yes: bool) -> None:
|
|
|
529
572
|
_reconcile(detection, config_dir, binary, before)
|
|
530
573
|
|
|
531
574
|
|
|
575
|
+
def _update_standalone(binary: str, assume_yes: bool) -> int:
|
|
576
|
+
"""`self update` for a standalone binary. Returns the process exit code.
|
|
577
|
+
|
|
578
|
+
Reconciliation is deliberately *not* the in-process kind the other modes
|
|
579
|
+
use. Replacing the executable means the next launch unpacks a different
|
|
580
|
+
distribution at a different path, so packages added to the old one are not
|
|
581
|
+
"removed by an upgrade" -- they are in a directory nothing will look at
|
|
582
|
+
again. The new binary is therefore asked to install them into its own
|
|
583
|
+
distribution, which it can only do after it has unpacked one.
|
|
584
|
+
"""
|
|
585
|
+
import functualize
|
|
586
|
+
from functualize._cli import manifest, self_update
|
|
587
|
+
|
|
588
|
+
config_dir = _config_dir()
|
|
589
|
+
# Captured before the replacement, for the same reason every other mode
|
|
590
|
+
# persists first: an update interrupted between the two loses the record of
|
|
591
|
+
# what to put back.
|
|
592
|
+
keep = sorted(set(manifest.recorded_additions(config_dir, binary)))
|
|
593
|
+
|
|
594
|
+
code = self_update.perform(
|
|
595
|
+
binary=Path(binary),
|
|
596
|
+
prefix=Path(sys.prefix),
|
|
597
|
+
current_version=functualize.__version__,
|
|
598
|
+
assume_yes=assume_yes,
|
|
599
|
+
echo=click.echo,
|
|
600
|
+
confirm=click.confirm,
|
|
601
|
+
)
|
|
602
|
+
if code != int(ExitCode.OK) or not keep:
|
|
603
|
+
return code
|
|
604
|
+
|
|
605
|
+
click.echo(f"Reinstalling {len(keep)} package(s) into the new distribution.")
|
|
606
|
+
failed: list[str] = []
|
|
607
|
+
for package in keep:
|
|
608
|
+
result = subprocess.run( # noqa: S603 - argv list, no shell
|
|
609
|
+
[binary, "builtin", "self", "install", package, "--yes"],
|
|
610
|
+
check=False,
|
|
611
|
+
)
|
|
612
|
+
if result.returncode != 0:
|
|
613
|
+
failed.append(package)
|
|
614
|
+
|
|
615
|
+
for package in failed:
|
|
616
|
+
click.echo(f" could not reinstall {package}", err=True)
|
|
617
|
+
if failed:
|
|
618
|
+
# Reported, not fatal: the update itself succeeded, and turning one
|
|
619
|
+
# unreachable package into a failed update would misdescribe the state
|
|
620
|
+
# of the installation.
|
|
621
|
+
click.echo(
|
|
622
|
+
f"{len(failed)} package(s) need reinstalling by hand.",
|
|
623
|
+
err=True,
|
|
624
|
+
)
|
|
625
|
+
return int(ExitCode.OK)
|
|
626
|
+
|
|
627
|
+
|
|
532
628
|
def _reconcile(
|
|
533
629
|
detection: Detection,
|
|
534
630
|
config_dir: Path,
|