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.
Files changed (272) hide show
  1. periplus/__init__.py +70 -0
  2. periplus/checkmap.py +80 -0
  3. periplus/cli.py +658 -0
  4. periplus/contract/schema/map.schema.json +138 -0
  5. periplus/contract/schema/navigation.schema.json +200 -0
  6. periplus/contract/schema/pack-file.schema.json +1405 -0
  7. periplus/contract/schema/pack-manifest.schema.json +245 -0
  8. periplus/contract/schema/project-settings.schema.json +85 -0
  9. periplus/contract/spec/engine.md +151 -0
  10. periplus/contract/spec/rules.md +352 -0
  11. periplus/detect.py +228 -0
  12. periplus/engine/__init__.py +5 -0
  13. periplus/engine/check.py +481 -0
  14. periplus/engine/emit.py +368 -0
  15. periplus/engine/packload.py +2321 -0
  16. periplus/engine/pattern.py +27 -0
  17. periplus/engine/rules.py +1946 -0
  18. periplus/engine/select.py +491 -0
  19. periplus/engine/source.py +172 -0
  20. periplus/engine/tree.py +424 -0
  21. periplus/errors.py +332 -0
  22. periplus/init.py +358 -0
  23. periplus/layout.py +28 -0
  24. periplus/manifest.py +498 -0
  25. periplus/map.py +364 -0
  26. periplus/navigate/__init__.py +1 -0
  27. periplus/navigate/find.py +143 -0
  28. periplus/navigate/ids.py +30 -0
  29. periplus/navigate/listing.py +42 -0
  30. periplus/navigate/mapfile.py +189 -0
  31. periplus/navigate/path.py +175 -0
  32. periplus/navigate/show.py +175 -0
  33. periplus/navigate/types.py +83 -0
  34. periplus/navigate/walk.py +138 -0
  35. periplus/packs/advancedqueue_basic@0.0.1/pack.yaml +14 -0
  36. periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type.yaml +9 -0
  37. periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_annotation.yaml +43 -0
  38. periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_attribute.yaml +43 -0
  39. periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent.yaml +8 -0
  40. periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent_attribute.yaml +43 -0
  41. periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call.yaml +8 -0
  42. periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call_attribute.yaml +43 -0
  43. periplus/packs/ai_basic@0.0.1/pack.yaml +15 -0
  44. periplus/packs/config_pages_basic@0.0.1/bundle/config_pages_bundle_config.yaml +54 -0
  45. periplus/packs/config_pages_basic@0.0.1/pack.yaml +11 -0
  46. periplus/packs/crop_basic@0.0.1/bundle/crop_bundle_config.yaml +54 -0
  47. periplus/packs/crop_basic@0.0.1/pack.yaml +11 -0
  48. periplus/packs/drupal@0.1.0/block/block_placement.yaml +25 -0
  49. periplus/packs/drupal@0.1.0/block/block_placement_code.yaml +58 -0
  50. periplus/packs/drupal@0.1.0/block/block_placement_config.yaml +53 -0
  51. periplus/packs/drupal@0.1.0/block/block_plugin.yaml +52 -0
  52. periplus/packs/drupal@0.1.0/block/block_plugin_altered.yaml +61 -0
  53. periplus/packs/drupal@0.1.0/block/block_plugin_annotation.yaml +69 -0
  54. periplus/packs/drupal@0.1.0/block/block_plugin_attribute.yaml +56 -0
  55. periplus/packs/drupal@0.1.0/block/block_plugin_content.yaml +46 -0
  56. periplus/packs/drupal@0.1.0/block/block_plugin_derived.yaml +110 -0
  57. periplus/packs/drupal@0.1.0/block/block_plugin_deriver.yaml +30 -0
  58. periplus/packs/drupal@0.1.0/block/block_type.yaml +14 -0
  59. periplus/packs/drupal@0.1.0/block/block_type_code.yaml +43 -0
  60. periplus/packs/drupal@0.1.0/block/block_type_config.yaml +45 -0
  61. periplus/packs/drupal@0.1.0/cache/cache.yaml +18 -0
  62. periplus/packs/drupal@0.1.0/cache/cache_backend.yaml +31 -0
  63. periplus/packs/drupal@0.1.0/component/sdc.yaml +19 -0
  64. periplus/packs/drupal@0.1.0/component/sdc_definition.yaml +25 -0
  65. periplus/packs/drupal@0.1.0/component/sdc_use.yaml +25 -0
  66. periplus/packs/drupal@0.1.0/config/config_object.yaml +65 -0
  67. periplus/packs/drupal@0.1.0/display/display.yaml +32 -0
  68. periplus/packs/drupal@0.1.0/display/display_code.yaml +53 -0
  69. periplus/packs/drupal@0.1.0/display/display_config.yaml +95 -0
  70. periplus/packs/drupal@0.1.0/entity/entity.yaml +33 -0
  71. periplus/packs/drupal@0.1.0/entity/entity_class.yaml +60 -0
  72. periplus/packs/drupal@0.1.0/entity/entity_save.yaml +27 -0
  73. periplus/packs/drupal@0.1.0/entity/entity_type_annotation.yaml +85 -0
  74. periplus/packs/drupal@0.1.0/entity/entity_type_attribute.yaml +76 -0
  75. periplus/packs/drupal@0.1.0/field/field_instance_code.yaml +40 -0
  76. periplus/packs/drupal@0.1.0/field/field_instance_config.yaml +58 -0
  77. periplus/packs/drupal@0.1.0/field/field_storage.yaml +37 -0
  78. periplus/packs/drupal@0.1.0/field/field_storage_code.yaml +39 -0
  79. periplus/packs/drupal@0.1.0/field/field_storage_config.yaml +24 -0
  80. periplus/packs/drupal@0.1.0/form/form.yaml +48 -0
  81. periplus/packs/drupal@0.1.0/form/form_class.yaml +50 -0
  82. periplus/packs/drupal@0.1.0/form/form_id.yaml +48 -0
  83. periplus/packs/drupal@0.1.0/form/form_methods.yaml +39 -0
  84. periplus/packs/drupal@0.1.0/hook/hook.yaml +8 -0
  85. periplus/packs/drupal@0.1.0/hook/hook_attribute.yaml +28 -0
  86. periplus/packs/drupal@0.1.0/module/module.yaml +8 -0
  87. periplus/packs/drupal@0.1.0/module/module_info.yaml +50 -0
  88. periplus/packs/drupal@0.1.0/pack.yaml +184 -0
  89. periplus/packs/drupal@0.1.0/plugin/plugin.yaml +17 -0
  90. periplus/packs/drupal@0.1.0/plugin/plugin_annotation.yaml +120 -0
  91. periplus/packs/drupal@0.1.0/recommendations/recommendations.yaml +14 -0
  92. periplus/packs/drupal@0.1.0/service/service.yaml +19 -0
  93. periplus/packs/drupal@0.1.0/service/service_autowire.yaml +28 -0
  94. periplus/packs/drupal@0.1.0/service/service_definition.yaml +24 -0
  95. periplus/packs/drupal@0.1.0/service/service_use.yaml +43 -0
  96. periplus/packs/drupal_basic@0.3.0/action/action.yaml +9 -0
  97. periplus/packs/drupal_basic@0.3.0/action/action_annotation.yaml +43 -0
  98. periplus/packs/drupal_basic@0.3.0/action/action_attribute.yaml +43 -0
  99. periplus/packs/drupal_basic@0.3.0/block/block_placement.yaml +16 -0
  100. periplus/packs/drupal_basic@0.3.0/block/block_placement_config.yaml +51 -0
  101. periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin.yaml +14 -0
  102. periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_annotation.yaml +47 -0
  103. periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_attribute.yaml +43 -0
  104. periplus/packs/drupal_basic@0.3.0/block_type/block_type.yaml +8 -0
  105. periplus/packs/drupal_basic@0.3.0/block_type/block_type_config.yaml +54 -0
  106. periplus/packs/drupal_basic@0.3.0/boundary/boundary_types.yaml +19 -0
  107. periplus/packs/drupal_basic@0.3.0/bundle/bundle.yaml +20 -0
  108. periplus/packs/drupal_basic@0.3.0/bundle/bundle_config.yaml +393 -0
  109. periplus/packs/drupal_basic@0.3.0/call/call.yaml +349 -0
  110. periplus/packs/drupal_basic@0.3.0/component/component.yaml +30 -0
  111. periplus/packs/drupal_basic@0.3.0/config/config_object.yaml +119 -0
  112. periplus/packs/drupal_basic@0.3.0/constraint/constraint.yaml +8 -0
  113. periplus/packs/drupal_basic@0.3.0/constraint/constraint_annotation.yaml +43 -0
  114. periplus/packs/drupal_basic@0.3.0/constraint/constraint_attribute.yaml +43 -0
  115. periplus/packs/drupal_basic@0.3.0/display/display.yaml +30 -0
  116. periplus/packs/drupal_basic@0.3.0/display/display_config.yaml +203 -0
  117. periplus/packs/drupal_basic@0.3.0/display/display_mode_config.yaml +60 -0
  118. periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_annotation.yaml +46 -0
  119. periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_attribute.yaml +43 -0
  120. periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_annotation.yaml +46 -0
  121. periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_attribute.yaml +43 -0
  122. periplus/packs/drupal_basic@0.3.0/entity_type/entity_type.yaml +16 -0
  123. periplus/packs/drupal_basic@0.3.0/field/field_instance_config.yaml +116 -0
  124. periplus/packs/drupal_basic@0.3.0/field/field_storage.yaml +21 -0
  125. periplus/packs/drupal_basic@0.3.0/field/field_storage_config.yaml +45 -0
  126. periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter.yaml +8 -0
  127. periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_annotation.yaml +46 -0
  128. periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_attribute.yaml +43 -0
  129. periplus/packs/drupal_basic@0.3.0/field_type/field_type_annotation.yaml +46 -0
  130. periplus/packs/drupal_basic@0.3.0/field_type/field_type_attribute.yaml +50 -0
  131. periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_annotation.yaml +46 -0
  132. periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_attribute.yaml +50 -0
  133. periplus/packs/drupal_basic@0.3.0/form/form_id.yaml +80 -0
  134. periplus/packs/drupal_basic@0.3.0/hook/hook.yaml +100 -0
  135. periplus/packs/drupal_basic@0.3.0/hook/hook_attribute.yaml +64 -0
  136. periplus/packs/drupal_basic@0.3.0/hook/hook_theme_include.yaml +34 -0
  137. periplus/packs/drupal_basic@0.3.0/library/library.yaml +12 -0
  138. periplus/packs/drupal_basic@0.3.0/library/library_definition.yaml +58 -0
  139. periplus/packs/drupal_basic@0.3.0/local_action/local_action.yaml +12 -0
  140. periplus/packs/drupal_basic@0.3.0/local_action/local_action_definition.yaml +49 -0
  141. periplus/packs/drupal_basic@0.3.0/mail/mail.yaml +8 -0
  142. periplus/packs/drupal_basic@0.3.0/mail/mail_attribute.yaml +43 -0
  143. periplus/packs/drupal_basic@0.3.0/media_source/media_source.yaml +8 -0
  144. periplus/packs/drupal_basic@0.3.0/media_source/media_source_annotation.yaml +43 -0
  145. periplus/packs/drupal_basic@0.3.0/media_source/media_source_attribute.yaml +79 -0
  146. periplus/packs/drupal_basic@0.3.0/menu_link/menu_link.yaml +18 -0
  147. periplus/packs/drupal_basic@0.3.0/menu_link/menu_link_definition.yaml +48 -0
  148. periplus/packs/drupal_basic@0.3.0/module/module.yaml +7 -0
  149. periplus/packs/drupal_basic@0.3.0/module/module_info.yaml +58 -0
  150. periplus/packs/drupal_basic@0.3.0/pack.yaml +528 -0
  151. periplus/packs/drupal_basic@0.3.0/permission/permission.yaml +7 -0
  152. periplus/packs/drupal_basic@0.3.0/permission/permission_definition.yaml +27 -0
  153. periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker.yaml +9 -0
  154. periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_annotation.yaml +43 -0
  155. periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_attribute.yaml +43 -0
  156. periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource.yaml +8 -0
  157. periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_annotation.yaml +43 -0
  158. periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_attribute.yaml +43 -0
  159. periplus/packs/drupal_basic@0.3.0/role/role.yaml +14 -0
  160. periplus/packs/drupal_basic@0.3.0/role/role_config.yaml +44 -0
  161. periplus/packs/drupal_basic@0.3.0/route/route.yaml +42 -0
  162. periplus/packs/drupal_basic@0.3.0/route/route_definition.yaml +113 -0
  163. periplus/packs/drupal_basic@0.3.0/route/route_options.yaml +55 -0
  164. periplus/packs/drupal_basic@0.3.0/route/route_requirements.yaml +108 -0
  165. periplus/packs/drupal_basic@0.3.0/service/service.yaml +56 -0
  166. periplus/packs/drupal_basic@0.3.0/service/service_definition.yaml +181 -0
  167. periplus/packs/drupal_basic@0.3.0/template/template.yaml +86 -0
  168. periplus/packs/drupal_basic@0.3.0/template/template_route_link.yaml +30 -0
  169. periplus/packs/drupal_basic@0.3.0/theme/theme.yaml +15 -0
  170. periplus/packs/drupal_basic@0.3.0/theme/theme_info.yaml +61 -0
  171. periplus/packs/drupal_basic@0.3.0/theme_hook/template_suggestion.yaml +29 -0
  172. periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook.yaml +87 -0
  173. periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook_render.yaml +20 -0
  174. periplus/packs/drupal_basic@0.3.0/url/url.yaml +88 -0
  175. periplus/packs/drupal_basic@0.3.0/views_plugin/views_field_attribute.yaml +80 -0
  176. periplus/packs/drupal_basic@0.3.0/views_plugin/views_filter_attribute.yaml +80 -0
  177. periplus/packs/drupal_basic@0.3.0/views_plugin/views_plugin.yaml +15 -0
  178. periplus/packs/drupal_basic@0.3.0/views_plugin/views_style_attribute.yaml +80 -0
  179. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment.yaml +12 -0
  180. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php.yaml +15 -0
  181. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php_array.yaml +52 -0
  182. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_twig.yaml +26 -0
  183. periplus/packs/drupal_js_basic@0.0.1/attachment/php_function_holder.yaml +13 -0
  184. periplus/packs/drupal_js_basic@0.0.1/behavior/behavior.yaml +198 -0
  185. periplus/packs/drupal_js_basic@0.0.1/pack.yaml +153 -0
  186. periplus/packs/drupal_js_basic@0.0.1/setting/setting.yaml +13 -0
  187. periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_js.yaml +15 -0
  188. periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_php.yaml +92 -0
  189. periplus/packs/drush_basic@0.0.1/command/command.yaml +15 -0
  190. periplus/packs/drush_basic@0.0.1/command/command_annotation.yaml +60 -0
  191. periplus/packs/drush_basic@0.0.1/command/command_attribute.yaml +88 -0
  192. periplus/packs/drush_basic@0.0.1/pack.yaml +29 -0
  193. periplus/packs/eck_basic@0.0.1/bundle/eck_bundle_config.yaml +56 -0
  194. periplus/packs/eck_basic@0.0.1/entity_type/eck_entity_type_config.yaml +26 -0
  195. periplus/packs/eck_basic@0.0.1/pack.yaml +28 -0
  196. periplus/packs/go@0.0.2/interfaces/satisfies.yaml +47 -0
  197. periplus/packs/go@0.0.2/pack.yaml +51 -0
  198. periplus/packs/go@0.0.2/types/find_declarations.yaml +137 -0
  199. periplus/packs/go@0.0.2/types/node_types.yaml +36 -0
  200. periplus/packs/go_basic@0.0.3/pack.yaml +104 -0
  201. periplus/packs/go_basic@0.0.3/rules/calls.yaml +25 -0
  202. periplus/packs/go_basic@0.0.3/rules/declarations.yaml +40 -0
  203. periplus/packs/go_basic@0.0.3/rules/imports.yaml +16 -0
  204. periplus/packs/go_basic@0.0.3/types/node_types.yaml +15 -0
  205. periplus/packs/js_basic@0.0.1/function/function.yaml +68 -0
  206. periplus/packs/js_basic@0.0.1/pack.yaml +50 -0
  207. periplus/packs/laravel_basic@0.0.5/pack.yaml +313 -0
  208. periplus/packs/laravel_basic@0.0.5/rules/classes.yaml +42 -0
  209. periplus/packs/laravel_basic@0.0.5/rules/config.yaml +229 -0
  210. periplus/packs/laravel_basic@0.0.5/rules/routes.yaml +210 -0
  211. periplus/packs/laravel_basic@0.0.5/rules/types.yaml +50 -0
  212. periplus/packs/laravel_basic@0.0.5/rules/views.yaml +100 -0
  213. periplus/packs/paragraphs_basic@0.0.1/bundle/paragraph_bundle_config.yaml +54 -0
  214. periplus/packs/paragraphs_basic@0.0.1/pack.yaml +12 -0
  215. periplus/packs/php@0.1.0/edges/edge_kinds.yaml +64 -0
  216. periplus/packs/php@0.1.0/edges/find_calls.yaml +34 -0
  217. periplus/packs/php@0.1.0/edges/find_imports.yaml +18 -0
  218. periplus/packs/php@0.1.0/edges/find_member_access.yaml +22 -0
  219. periplus/packs/php@0.1.0/edges/find_structure.yaml +38 -0
  220. periplus/packs/php@0.1.0/embedded/embedded_regions.yaml +19 -0
  221. periplus/packs/php@0.1.0/imports/imports.yaml +20 -0
  222. periplus/packs/php@0.1.0/pack.yaml +117 -0
  223. periplus/packs/php@0.1.0/resolution/excluded_tokens.yaml +23 -0
  224. periplus/packs/php@0.1.0/resolution/resolution.yaml +35 -0
  225. periplus/packs/php@0.1.0/types/find_declarations.yaml +102 -0
  226. periplus/packs/php@0.1.0/types/node_types.yaml +55 -0
  227. periplus/packs/php_basic@0.2.0/edges/edge_kinds.yaml +48 -0
  228. periplus/packs/php_basic@0.2.0/edges/find_structure.yaml +750 -0
  229. periplus/packs/php_basic@0.2.0/imports/imports.yaml +19 -0
  230. periplus/packs/php_basic@0.2.0/pack.yaml +94 -0
  231. periplus/packs/php_basic@0.2.0/types/find_declarations.yaml +267 -0
  232. periplus/packs/php_basic@0.2.0/types/node_types.yaml +55 -0
  233. periplus/packs/profile_basic@0.0.1/bundle/profile_bundle_config.yaml +54 -0
  234. periplus/packs/profile_basic@0.0.1/pack.yaml +12 -0
  235. periplus/packs/salesforce_basic@0.0.1/event/salesforce_event.yaml +36 -0
  236. periplus/packs/salesforce_basic@0.0.1/pack.yaml +31 -0
  237. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping.yaml +19 -0
  238. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping_config.yaml +83 -0
  239. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field.yaml +8 -0
  240. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field_annotation.yaml +43 -0
  241. periplus/packs/twig@0.0.1/pack.yaml +26 -0
  242. periplus/packs/twig@0.0.1/types/template.yaml +57 -0
  243. periplus/packs/twig_basic@0.1.0/pack.yaml +11 -0
  244. periplus/packs/twig_tweak_basic@0.0.1/pack.yaml +27 -0
  245. periplus/packs/twig_tweak_basic@0.0.1/rules/block.yaml +20 -0
  246. periplus/packs/twig_tweak_basic@0.0.1/rules/entity.yaml +35 -0
  247. periplus/packs/twig_tweak_basic@0.0.1/rules/field.yaml +21 -0
  248. periplus/packs/twig_tweak_basic@0.0.1/rules/menu.yaml +21 -0
  249. periplus/packs/webform_basic@0.0.1/bundle/webform_submission_bundle_config.yaml +54 -0
  250. periplus/packs/webform_basic@0.0.1/pack.yaml +20 -0
  251. periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler.yaml +8 -0
  252. periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler_annotation.yaml +43 -0
  253. periplus/packs/yaml@0.0.1/pack.yaml +72 -0
  254. periplus/packs/yaml@0.0.1/types/find_mapping_entry.yaml +21 -0
  255. periplus/packs/yaml@0.0.1/types/yaml_types.yaml +44 -0
  256. periplus/packs/yaml_basic@0.1.0/pack.yaml +10 -0
  257. periplus/packs.py +447 -0
  258. periplus/preflight.py +102 -0
  259. periplus/py.typed +0 -0
  260. periplus/report.py +596 -0
  261. periplus/resolution.py +355 -0
  262. periplus/settings.py +617 -0
  263. periplus/settings.stub.yml +99 -0
  264. periplus/spec.py +774 -0
  265. periplus/update.py +197 -0
  266. periplus/validate.py +655 -0
  267. periplus_map-0.2.1.dist-info/METADATA +566 -0
  268. periplus_map-0.2.1.dist-info/RECORD +272 -0
  269. periplus_map-0.2.1.dist-info/WHEEL +4 -0
  270. periplus_map-0.2.1.dist-info/entry_points.txt +2 -0
  271. periplus_map-0.2.1.dist-info/licenses/LICENSE +202 -0
  272. 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
+ )