periplus-map 0.2.1__py3-none-any.whl
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.
- periplus/__init__.py +70 -0
- periplus/checkmap.py +80 -0
- periplus/cli.py +658 -0
- periplus/contract/schema/map.schema.json +138 -0
- periplus/contract/schema/navigation.schema.json +200 -0
- periplus/contract/schema/pack-file.schema.json +1405 -0
- periplus/contract/schema/pack-manifest.schema.json +245 -0
- periplus/contract/schema/project-settings.schema.json +85 -0
- periplus/contract/spec/engine.md +151 -0
- periplus/contract/spec/rules.md +352 -0
- periplus/detect.py +228 -0
- periplus/engine/__init__.py +5 -0
- periplus/engine/check.py +481 -0
- periplus/engine/emit.py +368 -0
- periplus/engine/packload.py +2321 -0
- periplus/engine/pattern.py +27 -0
- periplus/engine/rules.py +1946 -0
- periplus/engine/select.py +491 -0
- periplus/engine/source.py +172 -0
- periplus/engine/tree.py +424 -0
- periplus/errors.py +332 -0
- periplus/init.py +358 -0
- periplus/layout.py +28 -0
- periplus/manifest.py +498 -0
- periplus/map.py +364 -0
- periplus/navigate/__init__.py +1 -0
- periplus/navigate/find.py +143 -0
- periplus/navigate/ids.py +30 -0
- periplus/navigate/listing.py +42 -0
- periplus/navigate/mapfile.py +189 -0
- periplus/navigate/path.py +175 -0
- periplus/navigate/show.py +175 -0
- periplus/navigate/types.py +83 -0
- periplus/navigate/walk.py +138 -0
- periplus/packs/advancedqueue_basic@0.0.1/pack.yaml +14 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type.yaml +9 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_annotation.yaml +43 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent.yaml +8 -0
- periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call.yaml +8 -0
- periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/pack.yaml +15 -0
- periplus/packs/config_pages_basic@0.0.1/bundle/config_pages_bundle_config.yaml +54 -0
- periplus/packs/config_pages_basic@0.0.1/pack.yaml +11 -0
- periplus/packs/crop_basic@0.0.1/bundle/crop_bundle_config.yaml +54 -0
- periplus/packs/crop_basic@0.0.1/pack.yaml +11 -0
- periplus/packs/drupal@0.1.0/block/block_placement.yaml +25 -0
- periplus/packs/drupal@0.1.0/block/block_placement_code.yaml +58 -0
- periplus/packs/drupal@0.1.0/block/block_placement_config.yaml +53 -0
- periplus/packs/drupal@0.1.0/block/block_plugin.yaml +52 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_altered.yaml +61 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_annotation.yaml +69 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_attribute.yaml +56 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_content.yaml +46 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_derived.yaml +110 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_deriver.yaml +30 -0
- periplus/packs/drupal@0.1.0/block/block_type.yaml +14 -0
- periplus/packs/drupal@0.1.0/block/block_type_code.yaml +43 -0
- periplus/packs/drupal@0.1.0/block/block_type_config.yaml +45 -0
- periplus/packs/drupal@0.1.0/cache/cache.yaml +18 -0
- periplus/packs/drupal@0.1.0/cache/cache_backend.yaml +31 -0
- periplus/packs/drupal@0.1.0/component/sdc.yaml +19 -0
- periplus/packs/drupal@0.1.0/component/sdc_definition.yaml +25 -0
- periplus/packs/drupal@0.1.0/component/sdc_use.yaml +25 -0
- periplus/packs/drupal@0.1.0/config/config_object.yaml +65 -0
- periplus/packs/drupal@0.1.0/display/display.yaml +32 -0
- periplus/packs/drupal@0.1.0/display/display_code.yaml +53 -0
- periplus/packs/drupal@0.1.0/display/display_config.yaml +95 -0
- periplus/packs/drupal@0.1.0/entity/entity.yaml +33 -0
- periplus/packs/drupal@0.1.0/entity/entity_class.yaml +60 -0
- periplus/packs/drupal@0.1.0/entity/entity_save.yaml +27 -0
- periplus/packs/drupal@0.1.0/entity/entity_type_annotation.yaml +85 -0
- periplus/packs/drupal@0.1.0/entity/entity_type_attribute.yaml +76 -0
- periplus/packs/drupal@0.1.0/field/field_instance_code.yaml +40 -0
- periplus/packs/drupal@0.1.0/field/field_instance_config.yaml +58 -0
- periplus/packs/drupal@0.1.0/field/field_storage.yaml +37 -0
- periplus/packs/drupal@0.1.0/field/field_storage_code.yaml +39 -0
- periplus/packs/drupal@0.1.0/field/field_storage_config.yaml +24 -0
- periplus/packs/drupal@0.1.0/form/form.yaml +48 -0
- periplus/packs/drupal@0.1.0/form/form_class.yaml +50 -0
- periplus/packs/drupal@0.1.0/form/form_id.yaml +48 -0
- periplus/packs/drupal@0.1.0/form/form_methods.yaml +39 -0
- periplus/packs/drupal@0.1.0/hook/hook.yaml +8 -0
- periplus/packs/drupal@0.1.0/hook/hook_attribute.yaml +28 -0
- periplus/packs/drupal@0.1.0/module/module.yaml +8 -0
- periplus/packs/drupal@0.1.0/module/module_info.yaml +50 -0
- periplus/packs/drupal@0.1.0/pack.yaml +184 -0
- periplus/packs/drupal@0.1.0/plugin/plugin.yaml +17 -0
- periplus/packs/drupal@0.1.0/plugin/plugin_annotation.yaml +120 -0
- periplus/packs/drupal@0.1.0/recommendations/recommendations.yaml +14 -0
- periplus/packs/drupal@0.1.0/service/service.yaml +19 -0
- periplus/packs/drupal@0.1.0/service/service_autowire.yaml +28 -0
- periplus/packs/drupal@0.1.0/service/service_definition.yaml +24 -0
- periplus/packs/drupal@0.1.0/service/service_use.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/action/action.yaml +9 -0
- periplus/packs/drupal_basic@0.3.0/action/action_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/action/action_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/block/block_placement.yaml +16 -0
- periplus/packs/drupal_basic@0.3.0/block/block_placement_config.yaml +51 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin.yaml +14 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_annotation.yaml +47 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/block_type/block_type.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/block_type/block_type_config.yaml +54 -0
- periplus/packs/drupal_basic@0.3.0/boundary/boundary_types.yaml +19 -0
- periplus/packs/drupal_basic@0.3.0/bundle/bundle.yaml +20 -0
- periplus/packs/drupal_basic@0.3.0/bundle/bundle_config.yaml +393 -0
- periplus/packs/drupal_basic@0.3.0/call/call.yaml +349 -0
- periplus/packs/drupal_basic@0.3.0/component/component.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/config/config_object.yaml +119 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/display/display.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/display/display_config.yaml +203 -0
- periplus/packs/drupal_basic@0.3.0/display/display_mode_config.yaml +60 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/entity_type.yaml +16 -0
- periplus/packs/drupal_basic@0.3.0/field/field_instance_config.yaml +116 -0
- periplus/packs/drupal_basic@0.3.0/field/field_storage.yaml +21 -0
- periplus/packs/drupal_basic@0.3.0/field/field_storage_config.yaml +45 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/field_type/field_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_type/field_type_attribute.yaml +50 -0
- periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_attribute.yaml +50 -0
- periplus/packs/drupal_basic@0.3.0/form/form_id.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook.yaml +100 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook_attribute.yaml +64 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook_theme_include.yaml +34 -0
- periplus/packs/drupal_basic@0.3.0/library/library.yaml +12 -0
- periplus/packs/drupal_basic@0.3.0/library/library_definition.yaml +58 -0
- periplus/packs/drupal_basic@0.3.0/local_action/local_action.yaml +12 -0
- periplus/packs/drupal_basic@0.3.0/local_action/local_action_definition.yaml +49 -0
- periplus/packs/drupal_basic@0.3.0/mail/mail.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/mail/mail_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source_attribute.yaml +79 -0
- periplus/packs/drupal_basic@0.3.0/menu_link/menu_link.yaml +18 -0
- periplus/packs/drupal_basic@0.3.0/menu_link/menu_link_definition.yaml +48 -0
- periplus/packs/drupal_basic@0.3.0/module/module.yaml +7 -0
- periplus/packs/drupal_basic@0.3.0/module/module_info.yaml +58 -0
- periplus/packs/drupal_basic@0.3.0/pack.yaml +528 -0
- periplus/packs/drupal_basic@0.3.0/permission/permission.yaml +7 -0
- periplus/packs/drupal_basic@0.3.0/permission/permission_definition.yaml +27 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker.yaml +9 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/role/role.yaml +14 -0
- periplus/packs/drupal_basic@0.3.0/role/role_config.yaml +44 -0
- periplus/packs/drupal_basic@0.3.0/route/route.yaml +42 -0
- periplus/packs/drupal_basic@0.3.0/route/route_definition.yaml +113 -0
- periplus/packs/drupal_basic@0.3.0/route/route_options.yaml +55 -0
- periplus/packs/drupal_basic@0.3.0/route/route_requirements.yaml +108 -0
- periplus/packs/drupal_basic@0.3.0/service/service.yaml +56 -0
- periplus/packs/drupal_basic@0.3.0/service/service_definition.yaml +181 -0
- periplus/packs/drupal_basic@0.3.0/template/template.yaml +86 -0
- periplus/packs/drupal_basic@0.3.0/template/template_route_link.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/theme/theme.yaml +15 -0
- periplus/packs/drupal_basic@0.3.0/theme/theme_info.yaml +61 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/template_suggestion.yaml +29 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook.yaml +87 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook_render.yaml +20 -0
- periplus/packs/drupal_basic@0.3.0/url/url.yaml +88 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_field_attribute.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_filter_attribute.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_plugin.yaml +15 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_style_attribute.yaml +80 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment.yaml +12 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php.yaml +15 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php_array.yaml +52 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_twig.yaml +26 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/php_function_holder.yaml +13 -0
- periplus/packs/drupal_js_basic@0.0.1/behavior/behavior.yaml +198 -0
- periplus/packs/drupal_js_basic@0.0.1/pack.yaml +153 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting.yaml +13 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_js.yaml +15 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_php.yaml +92 -0
- periplus/packs/drush_basic@0.0.1/command/command.yaml +15 -0
- periplus/packs/drush_basic@0.0.1/command/command_annotation.yaml +60 -0
- periplus/packs/drush_basic@0.0.1/command/command_attribute.yaml +88 -0
- periplus/packs/drush_basic@0.0.1/pack.yaml +29 -0
- periplus/packs/eck_basic@0.0.1/bundle/eck_bundle_config.yaml +56 -0
- periplus/packs/eck_basic@0.0.1/entity_type/eck_entity_type_config.yaml +26 -0
- periplus/packs/eck_basic@0.0.1/pack.yaml +28 -0
- periplus/packs/go@0.0.2/interfaces/satisfies.yaml +47 -0
- periplus/packs/go@0.0.2/pack.yaml +51 -0
- periplus/packs/go@0.0.2/types/find_declarations.yaml +137 -0
- periplus/packs/go@0.0.2/types/node_types.yaml +36 -0
- periplus/packs/go_basic@0.0.3/pack.yaml +104 -0
- periplus/packs/go_basic@0.0.3/rules/calls.yaml +25 -0
- periplus/packs/go_basic@0.0.3/rules/declarations.yaml +40 -0
- periplus/packs/go_basic@0.0.3/rules/imports.yaml +16 -0
- periplus/packs/go_basic@0.0.3/types/node_types.yaml +15 -0
- periplus/packs/js_basic@0.0.1/function/function.yaml +68 -0
- periplus/packs/js_basic@0.0.1/pack.yaml +50 -0
- periplus/packs/laravel_basic@0.0.5/pack.yaml +313 -0
- periplus/packs/laravel_basic@0.0.5/rules/classes.yaml +42 -0
- periplus/packs/laravel_basic@0.0.5/rules/config.yaml +229 -0
- periplus/packs/laravel_basic@0.0.5/rules/routes.yaml +210 -0
- periplus/packs/laravel_basic@0.0.5/rules/types.yaml +50 -0
- periplus/packs/laravel_basic@0.0.5/rules/views.yaml +100 -0
- periplus/packs/paragraphs_basic@0.0.1/bundle/paragraph_bundle_config.yaml +54 -0
- periplus/packs/paragraphs_basic@0.0.1/pack.yaml +12 -0
- periplus/packs/php@0.1.0/edges/edge_kinds.yaml +64 -0
- periplus/packs/php@0.1.0/edges/find_calls.yaml +34 -0
- periplus/packs/php@0.1.0/edges/find_imports.yaml +18 -0
- periplus/packs/php@0.1.0/edges/find_member_access.yaml +22 -0
- periplus/packs/php@0.1.0/edges/find_structure.yaml +38 -0
- periplus/packs/php@0.1.0/embedded/embedded_regions.yaml +19 -0
- periplus/packs/php@0.1.0/imports/imports.yaml +20 -0
- periplus/packs/php@0.1.0/pack.yaml +117 -0
- periplus/packs/php@0.1.0/resolution/excluded_tokens.yaml +23 -0
- periplus/packs/php@0.1.0/resolution/resolution.yaml +35 -0
- periplus/packs/php@0.1.0/types/find_declarations.yaml +102 -0
- periplus/packs/php@0.1.0/types/node_types.yaml +55 -0
- periplus/packs/php_basic@0.2.0/edges/edge_kinds.yaml +48 -0
- periplus/packs/php_basic@0.2.0/edges/find_structure.yaml +750 -0
- periplus/packs/php_basic@0.2.0/imports/imports.yaml +19 -0
- periplus/packs/php_basic@0.2.0/pack.yaml +94 -0
- periplus/packs/php_basic@0.2.0/types/find_declarations.yaml +267 -0
- periplus/packs/php_basic@0.2.0/types/node_types.yaml +55 -0
- periplus/packs/profile_basic@0.0.1/bundle/profile_bundle_config.yaml +54 -0
- periplus/packs/profile_basic@0.0.1/pack.yaml +12 -0
- periplus/packs/salesforce_basic@0.0.1/event/salesforce_event.yaml +36 -0
- periplus/packs/salesforce_basic@0.0.1/pack.yaml +31 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping.yaml +19 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping_config.yaml +83 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field.yaml +8 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field_annotation.yaml +43 -0
- periplus/packs/twig@0.0.1/pack.yaml +26 -0
- periplus/packs/twig@0.0.1/types/template.yaml +57 -0
- periplus/packs/twig_basic@0.1.0/pack.yaml +11 -0
- periplus/packs/twig_tweak_basic@0.0.1/pack.yaml +27 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/block.yaml +20 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/entity.yaml +35 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/field.yaml +21 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/menu.yaml +21 -0
- periplus/packs/webform_basic@0.0.1/bundle/webform_submission_bundle_config.yaml +54 -0
- periplus/packs/webform_basic@0.0.1/pack.yaml +20 -0
- periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler.yaml +8 -0
- periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler_annotation.yaml +43 -0
- periplus/packs/yaml@0.0.1/pack.yaml +72 -0
- periplus/packs/yaml@0.0.1/types/find_mapping_entry.yaml +21 -0
- periplus/packs/yaml@0.0.1/types/yaml_types.yaml +44 -0
- periplus/packs/yaml_basic@0.1.0/pack.yaml +10 -0
- periplus/packs.py +447 -0
- periplus/preflight.py +102 -0
- periplus/py.typed +0 -0
- periplus/report.py +596 -0
- periplus/resolution.py +355 -0
- periplus/settings.py +617 -0
- periplus/settings.stub.yml +99 -0
- periplus/spec.py +774 -0
- periplus/update.py +197 -0
- periplus/validate.py +655 -0
- periplus_map-0.2.1.dist-info/METADATA +566 -0
- periplus_map-0.2.1.dist-info/RECORD +272 -0
- periplus_map-0.2.1.dist-info/WHEEL +4 -0
- periplus_map-0.2.1.dist-info/entry_points.txt +2 -0
- periplus_map-0.2.1.dist-info/licenses/LICENSE +202 -0
- periplus_map-0.2.1.dist-info/licenses/NOTICE +2 -0
periplus/manifest.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""Pack manifests: reading one ``pack.yaml``, and walking ``depends`` into an ordered list.
|
|
2
|
+
|
|
3
|
+
Two questions, and they are two functions because they fail differently. What is in one manifest —
|
|
4
|
+
``load_manifest``, which raises, because one file has one failure. What order a pinned pack and
|
|
5
|
+
everything beneath it load in — ``resolve_pack_order``, which returns ``Problem`` records and never
|
|
6
|
+
raises, because one call has to report every unresolvable name, every duplicate and every cycle,
|
|
7
|
+
and an exception carries one. That is the same split ``settings.py`` and ``packs.py`` already
|
|
8
|
+
draw, for the same reason, and it is why this module ships one ``HarnessError`` subclass and also
|
|
9
|
+
returns problems.
|
|
10
|
+
|
|
11
|
+
**This module opens files inside pack directories. ``packs.py`` still does not.** The contract it
|
|
12
|
+
states is not amended and not weakened: resolving a pin, detecting a duplicate directory and
|
|
13
|
+
refusing a version mismatch remain judgments made from directory names alone, so that nothing has
|
|
14
|
+
to know what a ``folders`` block is before it knows what a pack is. This is the peer module that
|
|
15
|
+
runs *after* that, on the directories that stage produced. It is a consumer of ``PackCandidate``
|
|
16
|
+
and ``MatchedPack``, never an extension of the module that produces them, and nothing here is
|
|
17
|
+
imported by ``packs.py``.
|
|
18
|
+
|
|
19
|
+
What it does **not** do, because the validator does it:
|
|
20
|
+
|
|
21
|
+
* It does not check that the ``pack:`` inside a directory agrees with the ``<pack>@<version>``
|
|
22
|
+
outside. ``packs.py`` assigns that to the validator by name, and doing it here would put one
|
|
23
|
+
judgment in two places. What this module does instead is refuse to pick between them silently:
|
|
24
|
+
``LoadedPack`` carries the directory's ``PackName`` and the manifest's own ``pack`` side by side,
|
|
25
|
+
both unresolved, and the one rule that decides anything — *a ``depends`` entry resolves against
|
|
26
|
+
the pack half of a directory name* — is written down rather than inferred. A manifest that names
|
|
27
|
+
itself something else therefore loads under its directory's name and is refused later, which is
|
|
28
|
+
a report that is incomplete rather than one that lies.
|
|
29
|
+
* It enforces no line of ``pack-manifest.schema.json``. ``required: ["pack", "version"]`` is line 9
|
|
30
|
+
of that file and it is the validator's to enforce; a manifest missing either key loads here with
|
|
31
|
+
that field ``None``. A key whose value is not the declared shape — a ``depends`` that is a string
|
|
32
|
+
rather than a list of them — is dropped exactly as ``settings.py`` drops a mistyped settings
|
|
33
|
+
value, rather than coerced into a tuple of characters. Dropped and not warned about, because a
|
|
34
|
+
warning here is the validator's whole job arriving early and half-built.
|
|
35
|
+
|
|
36
|
+
The one thing a document *must* be is a mapping. That is not schema enforcement but the difference
|
|
37
|
+
between a file that can be read and one that cannot: an empty ``pack.yaml``, a list document and a
|
|
38
|
+
bare scalar are all valid YAML and none of them is a manifest, and there is no field to put them
|
|
39
|
+
in. They are ``ManifestUnreadable``, the same as bytes that will not decode.
|
|
40
|
+
|
|
41
|
+
Determinism, which is a stated property of the whole project and not a nicety here. Two runs over
|
|
42
|
+
one tree return the same order, and so do two runs whose inputs arrive in different order:
|
|
43
|
+
``depends`` entries are walked sorted and de-duplicated, pinned packs are walked sorted by name,
|
|
44
|
+
and nothing iterates a set or a directory in filesystem order. Sorting ``depends`` rather than
|
|
45
|
+
keeping the author's order costs nothing the engine spec would miss — contributions merge
|
|
46
|
+
additively and must not depend on load order — and buys an order that is a function of the graph
|
|
47
|
+
instead of a function of how somebody typed a list.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
53
|
+
from dataclasses import dataclass
|
|
54
|
+
from importlib.resources.abc import Traversable
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from ruamel.yaml import YAML
|
|
58
|
+
from ruamel.yaml.error import YAMLError
|
|
59
|
+
|
|
60
|
+
from periplus.errors import ExitCode, HarnessError, Problem
|
|
61
|
+
from periplus.packs import MatchedPack, PackCandidate, PackName
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"MANIFEST_FILENAME",
|
|
65
|
+
"LoadedPack",
|
|
66
|
+
"ManifestUnreadable",
|
|
67
|
+
"PackManifest",
|
|
68
|
+
"load_manifest",
|
|
69
|
+
"resolve_pack_order",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
#: The manifest's filename, one per pack directory. Declared here rather than in ``layout.py``,
|
|
73
|
+
#: which holds the three names a *project* is laid out by and exists because three modules read
|
|
74
|
+
#: them. This is a pack's own layout and one module reads it; the validator, when it wants it,
|
|
75
|
+
#: imports it from here the way ``packs.py`` re-exports ``PACKS_DIRECTORY``.
|
|
76
|
+
MANIFEST_FILENAME = "pack.yaml"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class PackManifest:
|
|
81
|
+
"""One ``pack.yaml``, as the fields this module reads plus everything else it did not.
|
|
82
|
+
|
|
83
|
+
``pack`` and ``version`` are optional although the schema requires both, because the schema is
|
|
84
|
+
a promise a validator keeps and this module is not that validator. A manifest that omits one,
|
|
85
|
+
or writes something that is not a string, carries ``None`` here — a fact, where a fabricated
|
|
86
|
+
empty string would be a value nobody wrote.
|
|
87
|
+
|
|
88
|
+
``depends`` is the only field this module acts on. It is ``()`` for a manifest that declares
|
|
89
|
+
none and for one whose ``depends`` is not a list of strings, which are two different files and
|
|
90
|
+
one behaviour: neither contributes an edge, and the validator separates them.
|
|
91
|
+
|
|
92
|
+
``document`` is the whole parsed mapping, carried rather than discarded so that the consumers
|
|
93
|
+
this reader exists for — ``periplus spec`` and framework detection — read ``folders``,
|
|
94
|
+
``files``, ``grammar`` and ``boundary`` without this module growing a field per block and
|
|
95
|
+
without a second parse of the same bytes. It is the parse result itself, not a forecast of what
|
|
96
|
+
somebody might want.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
pack: str | None
|
|
100
|
+
version: str | None
|
|
101
|
+
depends: tuple[str, ...]
|
|
102
|
+
document: Mapping[str, object]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True, slots=True)
|
|
106
|
+
class LoadedPack:
|
|
107
|
+
"""One pack directory and the manifest inside it, with both names left unresolved.
|
|
108
|
+
|
|
109
|
+
``name`` is parsed from the directory name and ``manifest.pack`` is what the file says it is.
|
|
110
|
+
They are both here and neither wins, for the reason the module docstring gives: choosing is the
|
|
111
|
+
validator's, and choosing silently is the failure this package refuses everywhere.
|
|
112
|
+
|
|
113
|
+
``directory`` is the ``Traversable`` for the pack directory, which is the thing a consumer
|
|
114
|
+
needs and the thing ``MatchedPack`` cannot give it — that record carries a ``str`` path,
|
|
115
|
+
deliberately, because ``packs.py`` never joins past a root. Rule files live under this
|
|
116
|
+
directory and a consumer opens them through this handle, so it works under an install where the
|
|
117
|
+
bundled root is not a ``Path``.
|
|
118
|
+
|
|
119
|
+
``path`` is the printable form, joined lexically from the root's own display exactly as
|
|
120
|
+
``MatchedPack.path`` is, so a pack named in this module's output and in a resolution report
|
|
121
|
+
reads the same in both.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
name: PackName
|
|
125
|
+
directory: Traversable
|
|
126
|
+
path: str
|
|
127
|
+
manifest: PackManifest
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ManifestUnreadable(HarnessError):
|
|
131
|
+
"""A pack manifest could not be turned into a mapping.
|
|
132
|
+
|
|
133
|
+
One class for five causes — the file is not there, the bytes could not be read, they could not
|
|
134
|
+
be decoded, the parser refused them, or the document is not a mapping — because they are one
|
|
135
|
+
thing to do about it: open the named file and fix it. ``detail`` names which one it was, so the
|
|
136
|
+
five stay separable for anyone reading ``--format json`` without becoming five exit codes that
|
|
137
|
+
mean the same remedy.
|
|
138
|
+
|
|
139
|
+
"Not there" is one of the five rather than a class of its own. A pack directory with no
|
|
140
|
+
``pack.yaml`` in it is not a pack, and the remedy is the same sentence: look at that path.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
code = ExitCode.UNREADABLE_MANIFEST
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def load_manifest(directory: Traversable) -> PackManifest:
|
|
147
|
+
"""Read the ``pack.yaml`` in one pack directory, or raise ``ManifestUnreadable``.
|
|
148
|
+
|
|
149
|
+
``YAML(typ='safe', pure=True)``, the same parser configuration ``settings.py`` uses and for the
|
|
150
|
+
same three reasons: ``safe`` refuses arbitrary object construction, ``safe`` is YAML 1.2 so a
|
|
151
|
+
duplicate key is refused rather than silently keeping the last value and a bare ``on`` stays the
|
|
152
|
+
string it was written as, and ``pure`` pins the pure-Python parser rather than letting an
|
|
153
|
+
optional C extension decide which one runs — an environment-dependent branch in a tool that
|
|
154
|
+
promises byte-identical output across machines.
|
|
155
|
+
|
|
156
|
+
``FileNotFoundError`` is caught before ``OSError``, which it subclasses, because the two have
|
|
157
|
+
different remedies and folding them would print "could not be read" about a file that was never
|
|
158
|
+
written. Everything else an ``OSError`` can be — a directory where the manifest belongs, a
|
|
159
|
+
permission refusal, a dangling symlink — is one message naming the path.
|
|
160
|
+
|
|
161
|
+
Nothing is validated. See the module docstring for which lines of the schema this deliberately
|
|
162
|
+
does not enforce and why.
|
|
163
|
+
"""
|
|
164
|
+
manifest = directory / MANIFEST_FILENAME
|
|
165
|
+
display = str(manifest)
|
|
166
|
+
yaml = YAML(typ="safe", pure=True)
|
|
167
|
+
try:
|
|
168
|
+
with manifest.open("r", encoding="utf-8") as stream:
|
|
169
|
+
document = yaml.load(stream)
|
|
170
|
+
except FileNotFoundError as error:
|
|
171
|
+
raise ManifestUnreadable(
|
|
172
|
+
f"the pack directory has no {MANIFEST_FILENAME}: {display}",
|
|
173
|
+
{"path": display, "cause": "missing", "detail": str(error)},
|
|
174
|
+
) from error
|
|
175
|
+
except YAMLError as error:
|
|
176
|
+
raise ManifestUnreadable(
|
|
177
|
+
f"the pack manifest could not be parsed: {display}",
|
|
178
|
+
{"path": display, "cause": "parser", "detail": str(error)},
|
|
179
|
+
) from error
|
|
180
|
+
except UnicodeDecodeError as error:
|
|
181
|
+
raise ManifestUnreadable(
|
|
182
|
+
f"the pack manifest is not valid UTF-8: {display}",
|
|
183
|
+
{"path": display, "cause": "encoding", "detail": str(error)},
|
|
184
|
+
) from error
|
|
185
|
+
except OSError as error:
|
|
186
|
+
raise ManifestUnreadable(
|
|
187
|
+
f"the pack manifest could not be read: {display}",
|
|
188
|
+
{"path": display, "cause": "read", "detail": str(error)},
|
|
189
|
+
) from error
|
|
190
|
+
|
|
191
|
+
if not isinstance(document, Mapping):
|
|
192
|
+
raise ManifestUnreadable(
|
|
193
|
+
f"the pack manifest is not a mapping: {display}",
|
|
194
|
+
{"path": display, "cause": "shape", "detail": type(document).__name__},
|
|
195
|
+
)
|
|
196
|
+
return PackManifest(
|
|
197
|
+
pack=_as_string(document.get("pack")),
|
|
198
|
+
version=_as_string(document.get("version")),
|
|
199
|
+
depends=_as_strings(document.get("depends")),
|
|
200
|
+
document=document,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _as_string(value: object) -> str | None:
|
|
205
|
+
"""A string, or nothing. A ``version:`` written unquoted as ``0.1`` is a float, not a
|
|
206
|
+
version."""
|
|
207
|
+
return value if isinstance(value, str) else None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _as_strings(value: object) -> tuple[str, ...]:
|
|
211
|
+
"""A list of strings, all of them, or nothing at all — never a tuple of characters."""
|
|
212
|
+
if isinstance(value, list | tuple) and all(isinstance(item, str) for item in value):
|
|
213
|
+
return tuple(value)
|
|
214
|
+
return ()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True, slots=True)
|
|
218
|
+
class _Directory:
|
|
219
|
+
"""One pack directory, reached the same way whether a pin named it or a ``depends`` found it.
|
|
220
|
+
|
|
221
|
+
It exists so that the two are one code path. A ``PackCandidate`` carries an optional
|
|
222
|
+
``PackName`` because most entries under a pack root are not packs; every directory that gets
|
|
223
|
+
this far has a name, and saying so in the type is what keeps a ``None`` from having to be
|
|
224
|
+
checked for on a branch that cannot happen and cannot be tested.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
name: PackName
|
|
228
|
+
directory: Traversable
|
|
229
|
+
path: str
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def resolve_pack_order(
|
|
233
|
+
pinned: Sequence[MatchedPack],
|
|
234
|
+
candidates: Sequence[PackCandidate],
|
|
235
|
+
) -> tuple[tuple[LoadedPack, ...], tuple[Problem, ...]]:
|
|
236
|
+
"""Every pinned pack and everything beneath it, dependencies first. Never raises.
|
|
237
|
+
|
|
238
|
+
The order is a depth-first post-order over the ``depends`` graph, entered at the pinned packs in
|
|
239
|
+
sorted name order and descending into each pack's dependencies in sorted, de-duplicated name
|
|
240
|
+
order. Dependencies therefore come out before the packs that declare them, every pack appears
|
|
241
|
+
exactly once however many packs reach it, and the whole sequence is a function of the graph
|
|
242
|
+
rather than of the order the arguments arrived in.
|
|
243
|
+
|
|
244
|
+
How a name becomes a directory, which is the one rule this module decides and so is written
|
|
245
|
+
here rather than left to be inferred:
|
|
246
|
+
|
|
247
|
+
* A **pinned** pack is the directory its pin matched. A pin names ``<pack>@<version>`` outright,
|
|
248
|
+
so it settles its own version and no search is made for it. Two pins carrying the same pack
|
|
249
|
+
half — ``php@0.1.0`` and ``php@0.2.0`` both pinned — are ``DEPENDS_DUPLICATED``, because the
|
|
250
|
+
graph is keyed by pack name and a name cannot be two packs at once.
|
|
251
|
+
* A **``depends``** entry is the one ``named`` candidate directory whose pack half equals it.
|
|
252
|
+
None is ``DEPENDS_UNMATCHED``; more than one is ``DEPENDS_DUPLICATED``, reported rather than
|
|
253
|
+
resolved, because the entry carries no version and nothing in the manifest says which was
|
|
254
|
+
meant.
|
|
255
|
+
|
|
256
|
+
A name that is also pinned resolves to the pinned directory. That falls out of the pins being
|
|
257
|
+
walked first and is kept deliberately: an explicit pin is a version somebody chose, and a
|
|
258
|
+
``depends`` entry is not, so the pin is the only stated preference in the room.
|
|
259
|
+
|
|
260
|
+
Each name is resolved once and its outcome remembered, failures included, so a pack five other
|
|
261
|
+
packs depend on produces one problem rather than five. What is reported is a fact about the
|
|
262
|
+
name, and a fact repeated per referrer is the same fact printed five times.
|
|
263
|
+
|
|
264
|
+
A failure never empties the result. A pack whose dependency did not resolve is still returned —
|
|
265
|
+
its own manifest was read — and a cycle is reported with the offending edge dropped, so what
|
|
266
|
+
comes back is the order of the graph without that edge. Both follow ``match_pins``, which
|
|
267
|
+
returns both halves of a duplicated pin and reports it: withholding the result would resolve by
|
|
268
|
+
omission the thing the problem exists to leave open.
|
|
269
|
+
"""
|
|
270
|
+
problems: list[Problem] = []
|
|
271
|
+
pinned_directories = _pinned_directories(pinned)
|
|
272
|
+
discovered = _discovered_directories(candidates)
|
|
273
|
+
resolved: dict[str, LoadedPack | None] = {}
|
|
274
|
+
order: list[LoadedPack] = []
|
|
275
|
+
for name in sorted(pinned_directories):
|
|
276
|
+
_walk(name, pinned_directories, discovered, resolved, order, problems)
|
|
277
|
+
return tuple(order), tuple(problems)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _pinned_directories(pinned: Sequence[MatchedPack]) -> dict[str, tuple[_Directory, ...]]:
|
|
281
|
+
"""The pinned packs grouped by the pack half of their name, each group in directory order.
|
|
282
|
+
|
|
283
|
+
A pin needs no search: ``match_pins`` matches a candidate when ``candidate.entry == pin`` and
|
|
284
|
+
only when its status is ``named``, so the pin *is* the entry, the parsed name comes with the
|
|
285
|
+
record, and the directory is that entry under the root the match names.
|
|
286
|
+
|
|
287
|
+
A group holds more than one entry when two pins share a pack half, and also when one pin
|
|
288
|
+
matched under two roots — which ``match_pins`` has already reported at ``PIN_DUPLICATED``. The
|
|
289
|
+
second report this produces is not that one restated: it says the pack *name* resolves to two
|
|
290
|
+
directories, which is what stops the graph, and it would be true of two different pins as well.
|
|
291
|
+
"""
|
|
292
|
+
grouped: dict[str, list[_Directory]] = {}
|
|
293
|
+
for match in pinned:
|
|
294
|
+
grouped.setdefault(match.name.pack, []).append(
|
|
295
|
+
_directory(match.root.traversable, match.root.display, match.pin, match.name)
|
|
296
|
+
)
|
|
297
|
+
return {name: _sorted_directories(group) for name, group in grouped.items()}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _discovered_directories(
|
|
301
|
+
candidates: Sequence[PackCandidate],
|
|
302
|
+
) -> dict[str, tuple[_Directory, ...]]:
|
|
303
|
+
"""Every ``named`` candidate grouped by its pack half, each group in directory order.
|
|
304
|
+
|
|
305
|
+
Only ``named`` candidates are here. An ``unnamed`` entry has no pack half for a ``depends`` to
|
|
306
|
+
equal, and a ``not_a_directory`` or ``indeterminate`` one is not a pack directory whatever it is
|
|
307
|
+
called — all three are already reported by discovery, and admitting one here would let a
|
|
308
|
+
``README.md`` under a pack root satisfy a dependency.
|
|
309
|
+
"""
|
|
310
|
+
grouped: dict[str, list[_Directory]] = {}
|
|
311
|
+
for candidate in candidates:
|
|
312
|
+
if candidate.status == "named" and candidate.name is not None:
|
|
313
|
+
grouped.setdefault(candidate.name.pack, []).append(
|
|
314
|
+
_directory(
|
|
315
|
+
candidate.root.traversable,
|
|
316
|
+
candidate.root.display,
|
|
317
|
+
candidate.entry,
|
|
318
|
+
candidate.name,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
return {name: _sorted_directories(group) for name, group in grouped.items()}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _directory(root: Traversable, display: str, entry: str, name: PackName) -> _Directory:
|
|
325
|
+
"""One pack directory: the handle it is opened through and the path it is printed as.
|
|
326
|
+
|
|
327
|
+
The handle is ``Traversable.joinpath``, which is how a bundled pack is reached on an install
|
|
328
|
+
where the root is not a ``Path``. The printed path is joined lexically from the root's own
|
|
329
|
+
display, the same join ``packs.py`` makes for ``MatchedPack.path``, so one directory reads
|
|
330
|
+
identically in a resolution report and here.
|
|
331
|
+
"""
|
|
332
|
+
return _Directory(name=name, directory=root / entry, path=str(Path(display) / entry))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _sorted_directories(group: Sequence[_Directory]) -> tuple[_Directory, ...]:
|
|
336
|
+
"""One group of directories, de-duplicated by printable path and sorted by it.
|
|
337
|
+
|
|
338
|
+
Sorted so that the directory a duplicate problem names first does not depend on which root was
|
|
339
|
+
listed first, and de-duplicated because two pins naming one directory are one directory: the
|
|
340
|
+
same pin appearing twice in a ``packs:`` list must not turn into a duplicate that does not
|
|
341
|
+
exist.
|
|
342
|
+
"""
|
|
343
|
+
by_path = {entry.path: entry for entry in group}
|
|
344
|
+
return tuple(by_path[path] for path in sorted(by_path))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _walk(
|
|
348
|
+
start: str,
|
|
349
|
+
pinned_directories: Mapping[str, tuple[_Directory, ...]],
|
|
350
|
+
discovered: Mapping[str, tuple[_Directory, ...]],
|
|
351
|
+
resolved: dict[str, LoadedPack | None],
|
|
352
|
+
order: list[LoadedPack],
|
|
353
|
+
problems: list[Problem],
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Depth-first from one name, appending each pack to ``order`` after all of its dependencies.
|
|
356
|
+
|
|
357
|
+
Iterative rather than recursive. The depth is a codebase's dependency depth and nothing here
|
|
358
|
+
would plausibly reach the interpreter's limit, but this function's contract is that it never
|
|
359
|
+
raises for anything a person's input can cause, and a hand-written chain of a thousand packs is
|
|
360
|
+
a person's input.
|
|
361
|
+
|
|
362
|
+
``on_path`` maps a name being expanded to its position on the stack, which is what turns a back
|
|
363
|
+
edge into a nameable cycle rather than a bare "there was one": the slice from that position is
|
|
364
|
+
the cycle, in the order it was entered. The edge is then dropped and the walk continues, so one
|
|
365
|
+
cycle produces one problem and the packs around it still come back ordered.
|
|
366
|
+
|
|
367
|
+
``on_path`` is tested before ``resolved``, and the order is load-bearing. A name enters
|
|
368
|
+
``resolved`` when it is expanded and reaches ``order`` when it is popped, so every name
|
|
369
|
+
currently on the path is already in ``resolved`` — testing that first would read a back edge as
|
|
370
|
+
a pack that is simply finished, and no cycle would ever be found.
|
|
371
|
+
"""
|
|
372
|
+
if start in resolved:
|
|
373
|
+
return
|
|
374
|
+
first = _resolve(start, pinned_directories, discovered, resolved, problems)
|
|
375
|
+
stack: list[tuple[str, tuple[str, ...], int]] = [(start, _dependencies_of(first), 0)]
|
|
376
|
+
on_path: dict[str, int] = {start: 0}
|
|
377
|
+
while stack:
|
|
378
|
+
name, dependencies, index = stack[-1]
|
|
379
|
+
if index == len(dependencies):
|
|
380
|
+
stack.pop()
|
|
381
|
+
del on_path[name]
|
|
382
|
+
finished = resolved[name]
|
|
383
|
+
if finished is not None:
|
|
384
|
+
order.append(finished)
|
|
385
|
+
continue
|
|
386
|
+
stack[-1] = (name, dependencies, index + 1)
|
|
387
|
+
child = dependencies[index]
|
|
388
|
+
if child in on_path:
|
|
389
|
+
problems.append(_cycle(entry for entry, _, _ in stack[on_path[child] :]))
|
|
390
|
+
continue
|
|
391
|
+
if child in resolved:
|
|
392
|
+
continue
|
|
393
|
+
loaded = _resolve(child, pinned_directories, discovered, resolved, problems)
|
|
394
|
+
on_path[child] = len(stack)
|
|
395
|
+
stack.append((child, _dependencies_of(loaded), 0))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _dependencies_of(pack: LoadedPack | None) -> tuple[str, ...]:
|
|
399
|
+
"""One pack's dependencies, sorted and de-duplicated. A pack that did not load has none.
|
|
400
|
+
|
|
401
|
+
Sorted so the emitted order is a function of the graph and not of how the list was typed;
|
|
402
|
+
de-duplicated because ``depends: [php, php]`` is one edge, and the schema does not say
|
|
403
|
+
otherwise — it sets no ``uniqueItems`` on the array.
|
|
404
|
+
"""
|
|
405
|
+
if pack is None:
|
|
406
|
+
return ()
|
|
407
|
+
return tuple(sorted(set(pack.manifest.depends)))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _resolve(
|
|
411
|
+
name: str,
|
|
412
|
+
pinned_directories: Mapping[str, tuple[_Directory, ...]],
|
|
413
|
+
discovered: Mapping[str, tuple[_Directory, ...]],
|
|
414
|
+
resolved: dict[str, LoadedPack | None],
|
|
415
|
+
problems: list[Problem],
|
|
416
|
+
) -> LoadedPack | None:
|
|
417
|
+
"""One name to one loaded pack, or to ``None`` and one problem. Answered once and remembered.
|
|
418
|
+
|
|
419
|
+
``None`` is cached exactly as a success is, which is what makes "one problem per name" hold: a
|
|
420
|
+
name that resolves to nothing is a settled answer, not an unanswered question to be retried by
|
|
421
|
+
the next pack that depends on it.
|
|
422
|
+
"""
|
|
423
|
+
if name in resolved:
|
|
424
|
+
return resolved[name]
|
|
425
|
+
directories = pinned_directories.get(name) or discovered.get(name) or ()
|
|
426
|
+
if not directories:
|
|
427
|
+
resolved[name] = None
|
|
428
|
+
problems.append(_unmatched(name, discovered))
|
|
429
|
+
return None
|
|
430
|
+
if len(directories) > 1:
|
|
431
|
+
resolved[name] = None
|
|
432
|
+
problems.append(_duplicated(name, directories))
|
|
433
|
+
return None
|
|
434
|
+
loaded = _load(directories[0], problems)
|
|
435
|
+
resolved[name] = loaded
|
|
436
|
+
return loaded
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _load(entry: _Directory, problems: list[Problem]) -> LoadedPack | None:
|
|
440
|
+
"""One directory read into a ``LoadedPack``, or ``None`` and the read failure as a problem.
|
|
441
|
+
|
|
442
|
+
This is the one place ``ManifestUnreadable`` is caught, and it is caught for the reason
|
|
443
|
+
``resolution.py`` catches ``SettingsUnreadable``: the exception is the right shape for one file
|
|
444
|
+
and the wrong shape for a walk that has to finish and report everything it found.
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
manifest = load_manifest(entry.directory)
|
|
448
|
+
except ManifestUnreadable as error:
|
|
449
|
+
problems.append(error.problem())
|
|
450
|
+
return None
|
|
451
|
+
return LoadedPack(
|
|
452
|
+
name=entry.name,
|
|
453
|
+
directory=entry.directory,
|
|
454
|
+
path=entry.path,
|
|
455
|
+
manifest=manifest,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _unmatched(name: str, discovered: Mapping[str, tuple[_Directory, ...]]) -> Problem:
|
|
460
|
+
"""A ``depends`` entry no pack directory carries, with the names that were on the path.
|
|
461
|
+
|
|
462
|
+
``available`` is every pack name discovery found, sorted, rather than a guess at which one was
|
|
463
|
+
meant. A near-miss list would need a distance rule, and a rule that decides ``twig`` was meant
|
|
464
|
+
by ``twigg`` will one day decide it was meant by ``php``.
|
|
465
|
+
"""
|
|
466
|
+
return Problem(
|
|
467
|
+
code=ExitCode.DEPENDS_UNMATCHED,
|
|
468
|
+
message=f"no pack directory on the search path is named {name!r}",
|
|
469
|
+
detail={"pack": name, "available": ", ".join(sorted(discovered))},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _duplicated(name: str, directories: Sequence[_Directory]) -> Problem:
|
|
474
|
+
"""One pack name carried by two directories. Both are named and neither is chosen."""
|
|
475
|
+
return Problem(
|
|
476
|
+
code=ExitCode.DEPENDS_DUPLICATED,
|
|
477
|
+
message=f"the pack name {name!r} is carried by more than one directory",
|
|
478
|
+
detail={
|
|
479
|
+
"pack": name,
|
|
480
|
+
"directories": ", ".join(entry.path for entry in directories),
|
|
481
|
+
},
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _cycle(names: Iterable[str]) -> Problem:
|
|
486
|
+
"""A ``depends`` chain that reaches back into itself, named in the order it was entered.
|
|
487
|
+
|
|
488
|
+
A one-name cycle is a pack that depends on itself. It needs no separate guard and gets no
|
|
489
|
+
separate code: it is the shortest back edge there is, the detector finds it the same way, and
|
|
490
|
+
the remedy is the same sentence.
|
|
491
|
+
"""
|
|
492
|
+
cycle = tuple(names)
|
|
493
|
+
written = " -> ".join((*cycle, cycle[0]))
|
|
494
|
+
return Problem(
|
|
495
|
+
code=ExitCode.DEPENDS_CYCLE,
|
|
496
|
+
message=f"the pack dependencies form a cycle: {written}",
|
|
497
|
+
detail={"cycle": written, "packs": ", ".join(sorted(cycle))},
|
|
498
|
+
)
|