slide2vec 7.1.2__tar.gz → 7.1.3__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.
Files changed (188) hide show
  1. {slide2vec-7.1.2 → slide2vec-7.1.3}/PKG-INFO +1 -1
  2. {slide2vec-7.1.2 → slide2vec-7.1.3}/pyproject.toml +3 -3
  3. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/__init__.py +1 -1
  4. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/__init__.py +2 -0
  5. slide2vec-7.1.3/slide2vec/encoders/code.py +102 -0
  6. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/inference.py +7 -2
  7. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_recipe.py +8 -0
  8. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/feature_identity.py +18 -10
  9. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/PKG-INFO +1 -1
  10. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/SOURCES.txt +2 -0
  11. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_resume.py +5 -0
  12. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_shard.py +2 -0
  13. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_stage.py +16 -0
  14. slide2vec-7.1.3/tests/test_encoder_code.py +241 -0
  15. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_plugins.py +9 -1
  16. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_feature_identity.py +106 -13
  17. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_output_consistency.py +3 -0
  18. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_pooled_identity_api.py +12 -1
  19. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_soma_migration.py +1 -0
  20. {slide2vec-7.1.2 → slide2vec-7.1.3}/LICENSE +0 -0
  21. {slide2vec-7.1.2 → slide2vec-7.1.3}/README.md +0 -0
  22. {slide2vec-7.1.2 → slide2vec-7.1.3}/setup.cfg +0 -0
  23. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/__main__.py +0 -0
  24. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/_cufile.py +0 -0
  25. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/api.py +0 -0
  26. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/artifacts.py +0 -0
  27. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/cli.py +0 -0
  28. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/configs/__init__.py +0 -0
  29. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/configs/default.yaml +0 -0
  30. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/configs/resources.py +0 -0
  31. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/cufile.json +0 -0
  32. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/__init__.py +0 -0
  33. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/dataset.py +0 -0
  34. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/tile_reader.py +0 -0
  35. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/tile_store.py +0 -0
  36. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/__init__.py +0 -0
  37. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/dense_image_worker.py +0 -0
  38. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/dense_worker.py +0 -0
  39. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/direct_embed_worker.py +0 -0
  40. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/image_worker.py +0 -0
  41. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/pin_gpu.py +0 -0
  42. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/pipeline_worker.py +0 -0
  43. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/worker_entry.py +0 -0
  44. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/base.py +0 -0
  45. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/__init__.py +0 -0
  46. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/conch.py +0 -0
  47. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/dinov2.py +0 -0
  48. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/dinov3.py +0 -0
  49. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/genbio.py +0 -0
  50. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/gigapath.py +0 -0
  51. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/gpfm.py +0 -0
  52. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/hibou.py +0 -0
  53. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/hoptimus.py +0 -0
  54. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/isight.py +0 -0
  55. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/lunit.py +0 -0
  56. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/mettle.py +0 -0
  57. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/midnight.py +0 -0
  58. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  59. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  60. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/case.py +0 -0
  61. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/loading.py +0 -0
  62. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/slide.py +0 -0
  63. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/types.py +0 -0
  64. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/mstar.py +0 -0
  65. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/musk.py +0 -0
  66. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/phikon.py +0 -0
  67. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/prism.py +0 -0
  68. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/prism2.py +0 -0
  69. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/prost40m.py +0 -0
  70. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/rudolfv2.py +0 -0
  71. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/titan.py +0 -0
  72. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/uni.py +0 -0
  73. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/virchow.py +0 -0
  74. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/waiv.py +0 -0
  75. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/registry.py +0 -0
  76. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/validation.py +0 -0
  77. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/progress.py +0 -0
  78. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/__init__.py +0 -0
  79. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/artifacts_collect.py +0 -0
  80. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/batching.py +0 -0
  81. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/cpu_budget.py +0 -0
  82. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_encode.py +0 -0
  83. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_encoder_input.py +0 -0
  84. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_reading.py +0 -0
  85. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_shard.py +0 -0
  86. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_stage.py +0 -0
  87. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_regions.py +0 -0
  88. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_shard.py +0 -0
  89. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_sliding.py +0 -0
  90. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_stage.py +0 -0
  91. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/distributed.py +0 -0
  92. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/distributed_stage.py +0 -0
  93. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/effective_encoder_input.py +0 -0
  94. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/embedding.py +0 -0
  95. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/embedding_persist.py +0 -0
  96. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/embedding_pipeline.py +0 -0
  97. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/encoder_input_contract.py +0 -0
  98. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/hierarchical.py +0 -0
  99. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/image_shard.py +0 -0
  100. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/image_specs.py +0 -0
  101. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/image_stage.py +0 -0
  102. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/manifest.py +0 -0
  103. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/model_settings.py +0 -0
  104. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/patient_pipeline.py +0 -0
  105. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/persist_callbacks.py +0 -0
  106. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/persistence.py +0 -0
  107. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  108. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/preprocessing.py +0 -0
  109. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/process_list.py +0 -0
  110. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/progress_bridge.py +0 -0
  111. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/registry.py +0 -0
  112. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/serialization.py +0 -0
  113. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/sharding.py +0 -0
  114. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/slide_encode.py +0 -0
  115. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/tiling.py +0 -0
  116. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/tiling_pipeline.py +0 -0
  117. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/types.py +0 -0
  118. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/worker_io.py +0 -0
  119. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/__init__.py +0 -0
  120. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/config.py +0 -0
  121. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/coordinates.py +0 -0
  122. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/log_utils.py +0 -0
  123. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/tiling_io.py +0 -0
  124. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/utils.py +0 -0
  125. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/dependency_links.txt +0 -0
  126. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/entry_points.txt +0 -0
  127. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/not-zip-safe +0 -0
  128. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/requires.txt +0 -0
  129. {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/top_level.txt +0 -0
  130. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_attention_extraction.py +0 -0
  131. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_benchmark_tooling.py +0 -0
  132. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_cli_noninteractive.py +0 -0
  133. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_cufile_compat_mode.py +0 -0
  134. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_encode_kit.py +0 -0
  135. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_encoder_input.py +0 -0
  136. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_extraction.py +0 -0
  137. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_reading.py +0 -0
  138. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_stage.py +0 -0
  139. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_regions.py +0 -0
  140. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_shard.py +0 -0
  141. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_sliding.py +0 -0
  142. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_source_spacing.py +0 -0
  143. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_worker.py +0 -0
  144. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dinov2_natimage.py +0 -0
  145. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dinov3_natimage.py +0 -0
  146. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_drop_in_encoders.py +0 -0
  147. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_capabilities.py +0 -0
  148. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_input_contract.py +0 -0
  149. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_preprocessing.py +0 -0
  150. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_provider_failures.py +0 -0
  151. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_registry.py +0 -0
  152. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_execution_options.py +0 -0
  153. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_gigapath.py +0 -0
  154. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_gpfm_genbio_heavy.py +0 -0
  155. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_hierarchical_encoding.py +0 -0
  156. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_hs2p5_integration.py +0 -0
  157. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_hs2p_package_cutover.py +0 -0
  158. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_image_shard.py +0 -0
  159. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_image_stage.py +0 -0
  160. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_inference_benchmark.py +0 -0
  161. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_isight.py +0 -0
  162. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_load_model_hf_auth.py +0 -0
  163. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_mascaret.py +0 -0
  164. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_mettle.py +0 -0
  165. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_on_slide_persisted.py +0 -0
  166. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patch_size_metadata.py +0 -0
  167. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patient_annotation_sampling.py +0 -0
  168. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patient_manifest.py +0 -0
  169. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patient_tiling_only.py +0 -0
  170. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_phaet.py +0 -0
  171. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_pooled_encoder_input.py +0 -0
  172. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_pooled_geometry.py +0 -0
  173. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_prism.py +0 -0
  174. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_prism2.py +0 -0
  175. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_process_list_performance.py +0 -0
  176. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_progress.py +0 -0
  177. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_regression_core.py +0 -0
  178. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_regression_inference.py +0 -0
  179. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_regression_models.py +0 -0
  180. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_rudolfv2.py +0 -0
  181. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_runtime_batching.py +0 -0
  182. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_sharding.py +0 -0
  183. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_slide_coordinate_preparation.py +0 -0
  184. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_slide_latents.py +0 -0
  185. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_tile_store.py +0 -0
  186. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_tiling_pipeline.py +0 -0
  187. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_titan.py +0 -0
  188. {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_worker_gpu_pinning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 7.1.2
3
+ Version: 7.1.3
4
4
  Summary: Embedding of whole slide images with Foundation Models
5
5
  Author-email: Clément Grisi <clement.grisi@radboudumc.nl>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slide2vec"
7
- version = "7.1.2"
7
+ version = "7.1.3"
8
8
  description = "Embedding of whole slide images with Foundation Models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -218,7 +218,7 @@ testpaths = [
218
218
  "tests",
219
219
  ]
220
220
  markers = [
221
- "heavy: real-weight foundation-model inference on CPU; minutes per test. Included in the PR suite and available separately through the manual heavy workflow (.github/workflows/nightly-heavy.yaml).",
221
+ "heavy: real-weight foundation-model inference on CPU; minutes per test. Excluded from the PR suite except the PRISM consistency test; run all of them through the manual heavy workflow (.github/workflows/nightly-heavy.yaml).",
222
222
  "gpu_integration: real one-GPU versus multi-GPU parity; requires at least two visible CUDA devices. Run explicitly with `CUDA_VISIBLE_DEVICES=0,1 python -m pytest -m gpu_integration --no-cov`.",
223
223
  ]
224
224
 
@@ -240,7 +240,7 @@ no_implicit_reexport = true
240
240
  max-line-length = 160
241
241
 
242
242
  [tool.bumpver]
243
- current_version = "7.1.2"
243
+ current_version = "7.1.3"
244
244
  version_pattern = "MAJOR.MINOR.PATCH"
245
245
  commit = false
246
246
  tag = false
@@ -36,7 +36,7 @@ from slide2vec.encoders import (
36
36
  )
37
37
 
38
38
 
39
- __version__ = "7.1.2"
39
+ __version__ = "7.1.3"
40
40
 
41
41
  __all__ = [
42
42
  "Model",
@@ -16,6 +16,7 @@ from slide2vec.encoders.base import (
16
16
  resolve_recommended_dynamic_img_size,
17
17
  resolve_requested_output_variant,
18
18
  )
19
+ from slide2vec.encoders.code import encoder_code
19
20
  from slide2vec.encoders.registry import (
20
21
  EncoderCapabilities,
21
22
  EncoderProviderDiagnostic,
@@ -42,6 +43,7 @@ __all__ = [
42
43
  "reshape_tokens_to_grid",
43
44
  "resolve_recommended_dynamic_img_size",
44
45
  "resolve_requested_output_variant",
46
+ "encoder_code",
45
47
  "encoder_registry",
46
48
  "list_encoder_provider_diagnostics",
47
49
  "normalize_patch_size",
@@ -0,0 +1,102 @@
1
+ """The code a registered encoder runs, as the feature identity records it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ import functools
7
+ import hashlib
8
+ from importlib import metadata as importlib_metadata
9
+ import inspect
10
+ import json
11
+ from pathlib import Path
12
+ import re
13
+ import sys
14
+ from typing import Any
15
+
16
+ from slide2vec.encoders.registry import encoder_registry
17
+
18
+
19
+ def encoder_code(name: str) -> dict[str, Any]:
20
+ """Return the code block of the registered encoder *name*.
21
+
22
+ ``object`` is the encoder class's ``module.qualname``. ``fingerprint`` is the
23
+ version of the non-editable installed distribution, other than slide2vec, whose
24
+ ``RECORD`` lists the file that defines the class; otherwise the SHA-256 of that
25
+ file, hashed on every call; ``None`` when there is no file (a class defined in a
26
+ notebook cell). ``source`` is the registry's ``source``. Reads the registry entry
27
+ only: the encoder is not instantiated and no weights are loaded.
28
+ """
29
+ encoder_cls = encoder_registry.require(name)
30
+ return {
31
+ "object": f"{encoder_cls.__module__}.{encoder_cls.__qualname__}",
32
+ "fingerprint": _fingerprint(encoder_cls),
33
+ "source": encoder_registry.info(name)["source"],
34
+ }
35
+
36
+
37
+ def _fingerprint(encoder_cls: type) -> str | None:
38
+ path = _source_file(encoder_cls)
39
+ if path is None:
40
+ return None
41
+ distribution = _installed_distribution(encoder_cls.__module__, path)
42
+ return _sha256(path) if distribution is None else distribution.version
43
+
44
+
45
+ def _installed_distribution(module: str, path: Path) -> importlib_metadata.Distribution | None:
46
+ """The non-editable installed distribution, other than slide2vec, whose ``RECORD``
47
+ lists *path*, or ``None``.
48
+
49
+ slide2vec is left out so that a built-in encoder's identity changes only when a
50
+ release changes its file. The distributions of a namespace package share its
51
+ top-level name and its install directory, so the top-level name only narrows the
52
+ candidates.
53
+ """
54
+ top = module.split(".", 1)[0]
55
+ for name in _packages_distributions(tuple(sys.path)).get(top, ()):
56
+ if re.sub(r"[-_.]+", "-", name).lower() == "slide2vec":
57
+ continue
58
+ try:
59
+ distribution = importlib_metadata.distribution(name)
60
+ except importlib_metadata.PackageNotFoundError:
61
+ continue
62
+ if distribution.read_text("RECORD") is None or _is_editable(distribution):
63
+ continue
64
+ try:
65
+ relative = path.relative_to(Path(distribution.locate_file("")).resolve())
66
+ except ValueError:
67
+ continue
68
+ if relative.as_posix() in {file.as_posix() for file in distribution.files or ()}:
69
+ return distribution
70
+ return None
71
+
72
+
73
+ def _is_editable(distribution) -> bool:
74
+ """Whether *distribution* is an editable install, as its ``direct_url.json`` records."""
75
+ try:
76
+ payload = json.loads(distribution.read_text("direct_url.json") or "{}")
77
+ except json.JSONDecodeError:
78
+ return False
79
+ return isinstance(payload, dict) and bool((payload.get("dir_info") or {}).get("editable"))
80
+
81
+
82
+ @functools.lru_cache(maxsize=1)
83
+ def _packages_distributions(path: tuple[str, ...]) -> Mapping[str, list[str]]:
84
+ """Top-level package -> distributions on the import *path*, scanned once per path."""
85
+ return importlib_metadata.packages_distributions()
86
+
87
+
88
+ def _source_file(encoder_cls: type) -> Path | None:
89
+ """The file that defines *encoder_cls*, or ``None`` (a notebook cell)."""
90
+ try:
91
+ path = Path(inspect.getsourcefile(encoder_cls) or inspect.getfile(encoder_cls))
92
+ except (TypeError, OSError):
93
+ return None
94
+ return path.resolve() if path.is_file() else None
95
+
96
+
97
+ def _sha256(path: Path) -> str:
98
+ digest = hashlib.sha256()
99
+ with path.open("rb") as handle:
100
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
101
+ digest.update(chunk)
102
+ return digest.hexdigest()
@@ -725,8 +725,8 @@ def aggregate_tiles(
725
725
  execution=execution,
726
726
  )
727
727
  latents = None
728
- # Aggregation encodes no tile: the tile geometry and transform are the ones
729
- # recorded with the tile artifact.
728
+ # Aggregation encodes no tile: the tile geometry, transform and tile encoder code
729
+ # are the ones recorded with the tile artifact.
730
730
  tile_identity = metadata.get("compatibility") or {}
731
731
  compatibility = {
732
732
  **feature_identity.pooled_feature_identity(model, execution=execution),
@@ -736,6 +736,11 @@ def aggregate_tiles(
736
736
  if field in tile_identity
737
737
  },
738
738
  }
739
+ # A slide run's tile sidecar records the tile encoder's code under `tile_encoder_code`.
740
+ compatibility.pop("tile_encoder_code", None)
741
+ tile_code = tile_identity.get("tile_encoder_code") or tile_identity.get("encoder_code")
742
+ if tile_code is not None:
743
+ compatibility["tile_encoder_code"] = tile_code
739
744
  slide_artifact = embedding.write_slide_embedding_artifact(
740
745
  artifact.sample_id,
741
746
  slide_embedding,
@@ -6,6 +6,7 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
+ from slide2vec.encoders.code import encoder_code
9
10
  from slide2vec.encoders.registry import resolve_encoder_output
10
11
  from slide2vec.runtime.model_settings import output_dtype_name, resolve_output_precision
11
12
 
@@ -67,6 +68,8 @@ class DenseImageRecipe:
67
68
  """Request-wide extraction facts combined with one parent-resolved image read plan."""
68
69
 
69
70
  encoder_name: str
71
+ #: The encoder's code block (:func:`slide2vec.encoders.encoder_code`).
72
+ encoder_code: dict[str, Any]
70
73
  output_variant: str
71
74
  reader_regime: str
72
75
  spacing_source: str
@@ -99,6 +102,7 @@ class DenseImageRecipe:
99
102
  def to_dict(self) -> dict[str, Any]:
100
103
  payload = {
101
104
  "encoder_name": self.encoder_name,
105
+ "encoder_code": self.encoder_code,
102
106
  "output_variant": self.output_variant,
103
107
  "reader_regime": self.reader_regime,
104
108
  "spacing_source": self.spacing_source,
@@ -142,6 +146,7 @@ class DenseImageRecipe:
142
146
  )
143
147
  recipe = self.to_dict()
144
148
  encoder_name = recipe.pop("encoder_name")
149
+ code = recipe.pop("encoder_code")
145
150
  output_variant = recipe.pop("output_variant")
146
151
  for field in read_plan.to_dict():
147
152
  recipe.pop(field, None)
@@ -149,6 +154,7 @@ class DenseImageRecipe:
149
154
  "sample_id": str(spec.sample_id),
150
155
  "image_path": str(Path(spec.image_path).expanduser().resolve()),
151
156
  "encoder_name": encoder_name,
157
+ "encoder_code": code,
152
158
  "output_variant": output_variant,
153
159
  **read_plan.to_dict(),
154
160
  **recipe,
@@ -158,6 +164,7 @@ class DenseImageRecipe:
158
164
  def from_dict(cls, payload: dict[str, Any]) -> "DenseImageRecipe":
159
165
  return cls(
160
166
  encoder_name=str(payload["encoder_name"]),
167
+ encoder_code=dict(payload["encoder_code"]),
161
168
  output_variant=str(payload["output_variant"]),
162
169
  reader_regime=str(payload["reader_regime"]),
163
170
  spacing_source=str(payload["spacing_source"]),
@@ -270,6 +277,7 @@ def resolve_dense_image_recipe(
270
277
  )
271
278
  return DenseImageRecipe(
272
279
  encoder_name=str(model.name),
280
+ encoder_code=encoder_code(model.name),
273
281
  output_variant=str(output["output_variant"]),
274
282
  reader_regime=str(reader_regime),
275
283
  spacing_source=(
@@ -11,6 +11,7 @@ from PIL import Image
11
11
  from transformers.image_processing_utils import BaseImageProcessor
12
12
 
13
13
  from slide2vec.encoders.base import ProcessorTransform
14
+ from slide2vec.encoders.code import encoder_code
14
15
  from slide2vec.encoders.registry import encoder_registry
15
16
  from slide2vec.runtime.model_settings import output_dtype_name, resolve_output_precision
16
17
  from slide2vec.runtime.preprocessing import iter_transform_steps
@@ -35,6 +36,7 @@ def pooled_feature_identity(
35
36
  if model.level != "tile" and model.name in encoder_registry:
36
37
  info = encoder_registry.info(model.name)
37
38
  identity["tile_encoder"] = info["tile_encoder"]
39
+ identity["tile_encoder_code"] = encoder_code(info["tile_encoder"])
38
40
  identity["tile_encoder_output_variant"] = info["tile_encoder_output_variant"]
39
41
  if info.get("aggregation_revision") is not None:
40
42
  identity["aggregation_revision"] = info["aggregation_revision"]
@@ -74,15 +76,16 @@ def dense_feature_identity(model, *, dense, execution) -> dict[str, Any]:
74
76
 
75
77
 
76
78
  def _encoder_identity(model, execution) -> dict[str, Any]:
77
- """The fields every path records: the encoder, its output variant, the precision."""
79
+ """The fields every path records: the encoder, its code, its output variant, the precision."""
78
80
  from slide2vec.runtime.process_list import resolved_process_list_output_variant
79
81
 
80
- return {
81
- "encoder_name": model.name,
82
- "output_variant": resolved_process_list_output_variant(model),
83
- # An unset inference precision runs without autocast, which is fp32.
84
- "precision": execution.precision or "fp32",
85
- }
82
+ identity: dict[str, Any] = {"encoder_name": model.name}
83
+ if model.name in encoder_registry:
84
+ identity["encoder_code"] = encoder_code(model.name)
85
+ identity["output_variant"] = resolved_process_list_output_variant(model)
86
+ # An unset inference precision runs without autocast, which is fp32.
87
+ identity["precision"] = execution.precision or "fp32"
88
+ return identity
86
89
 
87
90
 
88
91
  def deferred_transform_record(
@@ -253,11 +256,16 @@ class PooledResumeCheck:
253
256
  for field, values in differing.items()
254
257
  for leaf, old, new in _leaf_differences(field, *values)
255
258
  )
259
+ # A caller can request a recorded value, but not recorded code.
260
+ remedies = (
261
+ "Re-run into a new output_dir or delete the stale artifacts."
262
+ if differing.keys() & {"encoder_code", "tile_encoder_code"}
263
+ else "Re-run into a new output_dir, delete the stale artifacts, or request "
264
+ "the recorded values."
265
+ )
256
266
  raise ValueError(
257
267
  f"Cannot resume '{sample_id}': the existing {kind} embeddings at {path} "
258
- f"were computed with a different feature identity: {details}. Re-run into "
259
- "a new output_dir, delete the stale artifacts, or request the recorded "
260
- "values."
268
+ f"were computed with a different feature identity: {details}. {remedies}"
261
269
  )
262
270
  return not missing
263
271
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 7.1.2
3
+ Version: 7.1.3
4
4
  Summary: Embedding of whole slide images with Foundation Models
5
5
  Author-email: Clément Grisi <clement.grisi@radboudumc.nl>
6
6
  License-Expression: Apache-2.0
@@ -34,6 +34,7 @@ slide2vec/distributed/pipeline_worker.py
34
34
  slide2vec/distributed/worker_entry.py
35
35
  slide2vec/encoders/__init__.py
36
36
  slide2vec/encoders/base.py
37
+ slide2vec/encoders/code.py
37
38
  slide2vec/encoders/registry.py
38
39
  slide2vec/encoders/validation.py
39
40
  slide2vec/encoders/models/__init__.py
@@ -136,6 +137,7 @@ tests/test_dinov2_natimage.py
136
137
  tests/test_dinov3_natimage.py
137
138
  tests/test_drop_in_encoders.py
138
139
  tests/test_encoder_capabilities.py
140
+ tests/test_encoder_code.py
139
141
  tests/test_encoder_input_contract.py
140
142
  tests/test_encoder_plugins.py
141
143
  tests/test_encoder_preprocessing.py
@@ -9,6 +9,7 @@ import pytest
9
9
 
10
10
  from slide2vec.api import DenseImageOptions, ExecutionOptions, ImageSpec, Model
11
11
  from slide2vec.artifacts import dense_image_paths
12
+ from slide2vec.encoders import encoder_code
12
13
  from slide2vec.runtime.dense_image_stage import partition_dense_images_by_resume
13
14
  from slide2vec.runtime.dense_image_recipe import resolve_dense_image_recipe
14
15
  from slide2vec.runtime.dense_image_reading import DenseImageReadPlan
@@ -80,6 +81,7 @@ def test_dense_image_recipe_contains_the_complete_canonical_identity(tmp_path):
80
81
  )
81
82
  assert first.to_dict() == {
82
83
  "encoder_name": "virchow2",
84
+ "encoder_code": encoder_code("virchow2"),
83
85
  "output_variant": "cls",
84
86
  "reader_regime": "raster",
85
87
  "spacing_source": "explicit",
@@ -444,6 +446,7 @@ def test_every_recorded_compatibility_field_participates_in_resume(tmp_path):
444
446
  "sample_id",
445
447
  "image_path",
446
448
  "encoder_name",
449
+ "encoder_code",
447
450
  "output_variant",
448
451
  "reader_regime",
449
452
  "spacing_source",
@@ -493,6 +496,8 @@ def test_every_recorded_compatibility_field_participates_in_resume(tmp_path):
493
496
  recorded[field] = f"{value}-different"
494
497
  elif isinstance(value, list):
495
498
  recorded[field] = [value[0] + 1, *value[1:]]
499
+ elif isinstance(value, dict):
500
+ recorded[field] = {**value, "fingerprint": "different"}
496
501
  else:
497
502
  recorded[field] = value + 1
498
503
  out_dir = tmp_path / field
@@ -97,6 +97,7 @@ def _recipe(
97
97
  geometry = compute_dense_geometry(target_size=target_size, patch_size=(16, 16))
98
98
  return DenseImageRecipe(
99
99
  encoder_name="fake-encoder",
100
+ encoder_code={"object": "tests.FakeEncoder", "fingerprint": None, "source": ""},
100
101
  output_variant="default",
101
102
  reader_regime="raster",
102
103
  spacing_source="explicit",
@@ -317,6 +318,7 @@ def test_run_dense_image_shard_sidecar_records_extraction_geometry(tmp_path):
317
318
  "sample_id": "a",
318
319
  "image_path": str(Path(specs[0].image_path).resolve()),
319
320
  "encoder_name": "fake-encoder",
321
+ "encoder_code": {"object": "tests.FakeEncoder", "fingerprint": None, "source": ""},
320
322
  "output_variant": "default",
321
323
  "reader_regime": "raster",
322
324
  "spacing_source": "explicit",
@@ -385,6 +385,22 @@ def test_dense_region_sidecar_records_the_feature_identity(fake_backend, stub_re
385
385
  assert compatibility["read_level"] == 0 # the read plan is still recorded alongside
386
386
 
387
387
 
388
+ def test_dense_region_sidecar_records_the_code_of_a_registered_encoder(
389
+ fake_backend, stub_read_plan, tmp_path
390
+ ):
391
+ from slide2vec.encoders import encoder_code
392
+
393
+ [artifact] = dense_stage.embed_regions_dense(
394
+ _FakeModel(_encoder(), name="virchow2"),
395
+ [_regions(coords=((0, 0),))],
396
+ dense=_dense(),
397
+ execution=ExecutionOptions(output_dir=tmp_path, num_gpus=1, precision="fp32"),
398
+ )
399
+
400
+ compatibility = json.loads(artifact.metadata_path.read_text())["compatibility"]
401
+ assert compatibility["encoder_code"] == encoder_code("virchow2")
402
+
403
+
388
404
  def test_embed_regions_dense_resume_recomputes_regions_from_a_different_encoder(
389
405
  fake_backend, stub_read_plan, tmp_path
390
406
  ):
@@ -0,0 +1,241 @@
1
+ """The encoder code block the feature identity records under ``encoder_code``.
2
+
3
+ Ways it can go wrong, each covered below:
4
+
5
+ * computing the block instantiates the encoder or loads its weights;
6
+ * an edit to the file that defines the encoder leaves the block, or the comparison of
7
+ a recorded identity, unchanged;
8
+ * an installed encoder records its file's digest instead of its distribution's version,
9
+ or a namespace package shared by several distributions is attributed by its
10
+ top-level name instead of by the ``RECORD`` that lists the file;
11
+ * an editable install, or a file slide2vec itself installed, records a version that
12
+ does not change with the file;
13
+ * a class with no file (a notebook cell) raises instead of recording no fingerprint.
14
+
15
+ The installed-plugin path (a real ``pip install``) is covered in ``test_encoder_plugins.py``.
16
+ """
17
+
18
+ import hashlib
19
+ import importlib
20
+ import importlib.util
21
+ import json
22
+ from pathlib import Path
23
+ import sys
24
+ import types
25
+
26
+ from PIL import Image
27
+ import pytest
28
+
29
+ from slide2vec import ExecutionOptions, ImageSpec, Model
30
+ from slide2vec.artifacts import load_metadata
31
+ from slide2vec.encoders import encoder_code
32
+
33
+ ENCODER_MODULE = '''
34
+ from slide2vec import TorchTileEncoder, register_encoder
35
+
36
+
37
+ @register_encoder(
38
+ "{name}",
39
+ encode_dim=8,
40
+ input_size=32,
41
+ supports_variable_input_size=False,
42
+ supported_spacing_um=0.5,
43
+ precision="fp32",
44
+ source="/models/{name}.pt",
45
+ )
46
+ class CustomEncoder(TorchTileEncoder):
47
+ def __init__(self, *, output_variant=None):
48
+ raise AssertionError("encoder_code must not instantiate the encoder")
49
+ '''
50
+
51
+
52
+ WORKING_ENCODER_MODULE = '''
53
+ import torch
54
+
55
+ from slide2vec import TorchTileEncoder, register_encoder
56
+
57
+
58
+ @register_encoder(
59
+ "{name}",
60
+ encode_dim=3,
61
+ input_size=32,
62
+ supports_variable_input_size=False,
63
+ supported_spacing_um=0.5,
64
+ precision="fp32",
65
+ source="/models/{name}.pt",
66
+ )
67
+ class CustomEncoder(TorchTileEncoder):
68
+ def __init__(self, *, output_variant=None):
69
+ model = torch.nn.Sequential(torch.nn.AdaptiveAvgPool2d(1), torch.nn.Flatten())
70
+ super().__init__(
71
+ model, encode_dim=3, input_size=32, mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5),
72
+ output_variant=output_variant,
73
+ )
74
+ '''
75
+
76
+
77
+ def _import_module_file(path: Path, module_name: str, monkeypatch) -> None:
78
+ """Import *path* as *module_name*, as a script importing its encoder module does."""
79
+ spec = importlib.util.spec_from_file_location(module_name, path)
80
+ module = importlib.util.module_from_spec(spec)
81
+ monkeypatch.setitem(sys.modules, module_name, module)
82
+ spec.loader.exec_module(module)
83
+
84
+
85
+ def _sha256(path: Path) -> str:
86
+ return hashlib.sha256(path.read_bytes()).hexdigest()
87
+
88
+
89
+ def test_encoder_code_of_a_module_file_encoder_names_its_class_file_digest_and_source(
90
+ tmp_path, monkeypatch, isolated_encoder_registry
91
+ ):
92
+ path = tmp_path / "custom_encoders.py"
93
+ path.write_text(ENCODER_MODULE.format(name="custom-code"), encoding="utf-8")
94
+ _import_module_file(path, "custom_encoders", monkeypatch)
95
+
96
+ assert encoder_code("custom-code") == {
97
+ "object": "custom_encoders.CustomEncoder",
98
+ "fingerprint": _sha256(path),
99
+ "source": "/models/custom-code.pt",
100
+ }
101
+
102
+
103
+ def test_an_edit_to_a_module_file_encoder_is_a_feature_identity_difference(
104
+ tmp_path, monkeypatch, isolated_encoder_registry
105
+ ):
106
+ path = tmp_path / "edited_encoders.py"
107
+ path.write_text(WORKING_ENCODER_MODULE.format(name="edited-code"), encoding="utf-8")
108
+ _import_module_file(path, "edited_encoders", monkeypatch)
109
+ image_path = tmp_path / "image.png"
110
+ Image.new("RGB", (48, 48), color=(255, 255, 255)).save(image_path)
111
+ model = Model.from_preset("edited-code", device="cpu")
112
+ execution = ExecutionOptions(
113
+ output_dir=tmp_path / "out", precision="fp32", num_gpus=1, num_workers_per_gpu=0
114
+ )
115
+ (artifact,) = model.embed_images(
116
+ [ImageSpec(sample_id="image", image_path=image_path)], execution=execution
117
+ )
118
+ recorded = load_metadata(artifact.metadata_path)["compatibility"]
119
+ recorded_code = {
120
+ "object": "edited_encoders.CustomEncoder",
121
+ "fingerprint": _sha256(path),
122
+ "source": "/models/edited-code.pt",
123
+ }
124
+
125
+ assert recorded["encoder_code"] == recorded_code
126
+ assert model.pooled_identity_differences(
127
+ recorded, preprocessing=None, execution=execution
128
+ ) == {}
129
+
130
+ path.write_text(path.read_text(encoding="utf-8") + "\n# An edit.\n", encoding="utf-8")
131
+
132
+ assert model.pooled_identity_differences(
133
+ recorded, preprocessing=None, execution=execution
134
+ ) == {"encoder_code": (recorded_code, {**recorded_code, "fingerprint": _sha256(path)})}
135
+
136
+
137
+ #: A namespace package (no ``__init__.py``) that two installed distributions share, as
138
+ #: ``protobuf`` and ``googleapis-common-protos`` share ``google``.
139
+ NAMESPACE = "slide2vec_test_namespace"
140
+
141
+
142
+ def _install(site: Path, distribution: str, version: str, module: str, *, direct_url=None):
143
+ """Install ``<NAMESPACE>/<module>.py`` into *site* as *distribution*, listed in its RECORD."""
144
+ package = site / NAMESPACE
145
+ package.mkdir(parents=True, exist_ok=True)
146
+ (package / f"{module}.py").write_text(
147
+ ENCODER_MODULE.format(name=f"installed-{module}"), encoding="utf-8"
148
+ )
149
+ info = site / f"{distribution.replace('-', '_')}-{version}.dist-info"
150
+ info.mkdir()
151
+ (info / "METADATA").write_text(
152
+ f"Metadata-Version: 2.1\nName: {distribution}\nVersion: {version}\n", encoding="utf-8"
153
+ )
154
+ (info / "RECORD").write_text(
155
+ f"{NAMESPACE}/{module}.py,,\n{info.name}/METADATA,,\n{info.name}/RECORD,,\n",
156
+ encoding="utf-8",
157
+ )
158
+ if direct_url is not None:
159
+ (info / "direct_url.json").write_text(json.dumps(direct_url), encoding="utf-8")
160
+
161
+
162
+ @pytest.fixture
163
+ def site(tmp_path, monkeypatch, isolated_encoder_registry):
164
+ """An extra site directory on ``sys.path``; its namespace modules are unloaded after."""
165
+ site = tmp_path / "site-packages"
166
+ site.mkdir()
167
+ monkeypatch.syspath_prepend(str(site))
168
+ yield site
169
+ for name in [name for name in sys.modules if name.split(".", 1)[0] == NAMESPACE]:
170
+ sys.modules.pop(name)
171
+
172
+
173
+ def _import_installed(module: str) -> None:
174
+ importlib.import_module(f"{NAMESPACE}.{module}")
175
+
176
+
177
+ def test_encoder_code_of_an_installed_encoder_is_the_version_of_the_distribution_listing_it(site):
178
+ """Distributions of a namespace package share its top-level name: their RECORD decides."""
179
+ _install(site, "slide2vec-test-alpha", "1.0", "alpha")
180
+ _install(site, "slide2vec-test-beta", "2.0", "beta")
181
+ (site / NAMESPACE / "gamma.py").write_text(
182
+ ENCODER_MODULE.format(name="installed-gamma"), encoding="utf-8"
183
+ )
184
+ for module in ("alpha", "beta", "gamma"):
185
+ _import_installed(module)
186
+
187
+ assert {
188
+ module: encoder_code(f"installed-{module}")["fingerprint"]
189
+ for module in ("alpha", "beta", "gamma")
190
+ } == {
191
+ "alpha": "1.0",
192
+ "beta": "2.0",
193
+ # A file no distribution lists is not installed code: its digest pins it.
194
+ "gamma": _sha256(site / NAMESPACE / "gamma.py"),
195
+ }
196
+
197
+
198
+ def test_encoder_code_of_an_editable_install_is_the_digest_of_its_file(site):
199
+ """An editable install runs the work tree: its version does not change with an edit."""
200
+ _install(
201
+ site, "slide2vec-test-editable", "3.0", "delta",
202
+ direct_url={"url": site.as_uri(), "dir_info": {"editable": True}},
203
+ )
204
+ _import_installed("delta")
205
+
206
+ assert encoder_code("installed-delta")["fingerprint"] == _sha256(site / NAMESPACE / "delta.py")
207
+
208
+
209
+ def test_encoder_code_of_a_file_slide2vec_installed_is_its_digest(site):
210
+ """A built-in's identity changes when a release changes its file, not on every release."""
211
+ _install(site, "slide2vec", "99.0", "epsilon")
212
+ _import_installed("epsilon")
213
+
214
+ assert encoder_code("installed-epsilon")["fingerprint"] == _sha256(
215
+ site / NAMESPACE / "epsilon.py"
216
+ )
217
+
218
+
219
+ def test_encoder_code_of_a_built_in_encoder_is_the_digest_of_its_file():
220
+ import slide2vec.encoders.models.virchow as virchow
221
+
222
+ assert encoder_code("virchow2") == {
223
+ "object": "slide2vec.encoders.models.virchow.Virchow2",
224
+ "fingerprint": _sha256(Path(virchow.__file__)),
225
+ "source": "paige-ai/Virchow2",
226
+ }
227
+
228
+
229
+ def test_encoder_code_of_an_encoder_with_no_file_records_no_fingerprint(
230
+ monkeypatch, isolated_encoder_registry
231
+ ):
232
+ """A class defined in a notebook cell lives in a module that has no file."""
233
+ module = types.ModuleType("notebook_cell")
234
+ monkeypatch.setitem(sys.modules, "notebook_cell", module)
235
+ exec(compile(ENCODER_MODULE.format(name="cell-code"), "<cell>", "exec"), module.__dict__)
236
+
237
+ assert encoder_code("cell-code") == {
238
+ "object": "notebook_cell.CustomEncoder",
239
+ "fingerprint": None,
240
+ "source": "/models/cell-code.pt",
241
+ }
@@ -155,7 +155,7 @@ from threading import Barrier
155
155
 
156
156
  import slide2vec
157
157
  from slide2vec import Model, list_models
158
- from slide2vec.encoders import encoder_registry
158
+ from slide2vec.encoders import encoder_code, encoder_registry
159
159
  from slide2vec.runtime.serialization import serialize_model
160
160
 
161
161
  discovered = Path(os.environ["PLUGIN_DISCOVERED_SENTINEL"])
@@ -201,6 +201,14 @@ assert encoder_registry.info("private-beta") == {
201
201
  "source": "private-org/private-beta",
202
202
  }
203
203
 
204
+ # The installed distribution pins its encoders' code, without constructing them.
205
+ assert encoder_code("private-alpha") == {
206
+ "object": "private_encoder_plugin.register_presets.<locals>.PrivateAlpha",
207
+ "fingerprint": "1.0.0",
208
+ "source": "/models/private-alpha.pt",
209
+ }
210
+ assert not constructed.exists()
211
+
204
212
  model = Model.from_preset("private-alpha", device="cpu")
205
213
  assert model.name == "private-alpha"
206
214
  assert model.level == "tile"