edsl 0.1.47__py3-none-any.whl → 0.1.48__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.
- edsl/__init__.py +44 -39
- edsl/__version__.py +1 -1
- edsl/agents/__init__.py +4 -2
- edsl/agents/{Agent.py → agent.py} +442 -152
- edsl/agents/{AgentList.py → agent_list.py} +220 -162
- edsl/agents/descriptors.py +46 -7
- edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
- edsl/base/__init__.py +75 -0
- edsl/base/base_class.py +1303 -0
- edsl/base/data_transfer_models.py +114 -0
- edsl/base/enums.py +215 -0
- edsl/base.py +8 -0
- edsl/buckets/__init__.py +25 -0
- edsl/buckets/bucket_collection.py +324 -0
- edsl/buckets/model_buckets.py +206 -0
- edsl/buckets/token_bucket.py +502 -0
- edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
- edsl/buckets/token_bucket_client.py +509 -0
- edsl/caching/__init__.py +20 -0
- edsl/caching/cache.py +814 -0
- edsl/caching/cache_entry.py +427 -0
- edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
- edsl/caching/exceptions.py +24 -0
- edsl/caching/orm.py +30 -0
- edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
- edsl/caching/sql_dict.py +441 -0
- edsl/config/__init__.py +8 -0
- edsl/config/config_class.py +177 -0
- edsl/config.py +4 -176
- edsl/conversation/Conversation.py +7 -7
- edsl/conversation/car_buying.py +4 -4
- edsl/conversation/chips.py +6 -6
- edsl/coop/__init__.py +25 -2
- edsl/coop/coop.py +303 -67
- edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
- edsl/coop/exceptions.py +62 -0
- edsl/coop/price_fetcher.py +126 -0
- edsl/coop/utils.py +89 -24
- edsl/data_transfer_models.py +5 -72
- edsl/dataset/__init__.py +10 -0
- edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
- edsl/{results/DatasetExportMixin.py → dataset/dataset_operations_mixin.py} +606 -122
- edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
- edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
- edsl/{results → dataset/display}/table_renderers.py +58 -2
- edsl/{results → dataset}/file_exports.py +4 -5
- edsl/{results → dataset}/smart_objects.py +2 -2
- edsl/enums.py +5 -205
- edsl/inference_services/__init__.py +5 -0
- edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
- edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
- edsl/inference_services/data_structures.py +3 -2
- edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
- edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
- edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
- edsl/inference_services/registry.py +4 -41
- edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
- edsl/inference_services/services/__init__.py +31 -0
- edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
- edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
- edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
- edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
- edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
- edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
- edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
- edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
- edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
- edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
- edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +3 -7
- edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
- edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
- edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
- edsl/inference_services/write_available.py +1 -2
- edsl/instructions/__init__.py +6 -0
- edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
- edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
- edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
- edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
- edsl/interviews/__init__.py +4 -0
- edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
- edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
- edsl/interviews/interview.py +638 -0
- edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
- edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
- edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
- edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
- edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
- edsl/invigilators/__init__.py +38 -0
- edsl/invigilators/invigilator_base.py +477 -0
- edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
- edsl/invigilators/prompt_constructor.py +476 -0
- edsl/{agents → invigilators}/prompt_helpers.py +2 -1
- edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
- edsl/{agents → invigilators}/question_option_processor.py +96 -21
- edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
- edsl/jobs/__init__.py +7 -1
- edsl/jobs/async_interview_runner.py +99 -35
- edsl/jobs/check_survey_scenario_compatibility.py +7 -5
- edsl/jobs/data_structures.py +153 -22
- edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
- edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
- edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
- edsl/jobs/{Jobs.py → jobs.py} +313 -167
- edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
- edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +19 -17
- edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
- edsl/jobs/jobs_pricing_estimation.py +347 -0
- edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
- edsl/jobs/jobs_runner_asyncio.py +282 -0
- edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
- edsl/jobs/results_exceptions_handler.py +2 -2
- edsl/key_management/__init__.py +28 -0
- edsl/key_management/key_lookup.py +161 -0
- edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
- edsl/key_management/key_lookup_collection.py +82 -0
- edsl/key_management/models.py +218 -0
- edsl/language_models/__init__.py +7 -2
- edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
- edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
- edsl/language_models/language_model.py +1080 -0
- edsl/language_models/model.py +10 -25
- edsl/language_models/{ModelList.py → model_list.py} +9 -14
- edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
- edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
- edsl/language_models/repair.py +4 -4
- edsl/language_models/utilities.py +4 -4
- edsl/notebooks/__init__.py +3 -1
- edsl/notebooks/{Notebook.py → notebook.py} +7 -8
- edsl/prompts/__init__.py +1 -1
- edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
- edsl/prompts/{Prompt.py → prompt.py} +101 -95
- edsl/questions/HTMLQuestion.py +1 -1
- edsl/questions/__init__.py +154 -25
- edsl/questions/answer_validator_mixin.py +1 -1
- edsl/questions/compose_questions.py +4 -3
- edsl/questions/derived/question_likert_five.py +166 -0
- edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
- edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
- edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
- edsl/questions/descriptors.py +24 -30
- edsl/questions/loop_processor.py +65 -19
- edsl/questions/question_base.py +881 -0
- edsl/questions/question_base_gen_mixin.py +15 -16
- edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
- edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
- edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
- edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
- edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
- edsl/questions/question_free_text.py +282 -0
- edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
- edsl/questions/{QuestionList.py → question_list.py} +6 -7
- edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
- edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
- edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
- edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
- edsl/questions/question_registry.py +4 -9
- edsl/questions/register_questions_meta.py +8 -4
- edsl/questions/response_validator_abc.py +17 -16
- edsl/results/__init__.py +4 -1
- edsl/{exceptions/results.py → results/exceptions.py} +1 -1
- edsl/results/report.py +197 -0
- edsl/results/{Result.py → result.py} +131 -45
- edsl/results/{Results.py → results.py} +365 -220
- edsl/results/results_selector.py +344 -25
- edsl/scenarios/__init__.py +30 -3
- edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
- edsl/scenarios/directory_scanner.py +156 -13
- edsl/scenarios/document_chunker.py +186 -0
- edsl/scenarios/exceptions.py +101 -0
- edsl/scenarios/file_methods.py +2 -3
- edsl/scenarios/{FileStore.py → file_store.py} +275 -189
- edsl/scenarios/handlers/__init__.py +14 -14
- edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
- edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
- edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
- edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
- edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
- edsl/scenarios/handlers/latex_file_store.py +5 -0
- edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
- edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
- edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
- edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
- edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
- edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
- edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
- edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
- edsl/scenarios/scenario.py +928 -0
- edsl/scenarios/scenario_join.py +18 -5
- edsl/scenarios/{ScenarioList.py → scenario_list.py} +294 -106
- edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
- edsl/scenarios/scenario_selector.py +5 -1
- edsl/study/ObjectEntry.py +2 -2
- edsl/study/SnapShot.py +5 -5
- edsl/study/Study.py +18 -19
- edsl/study/__init__.py +6 -4
- edsl/surveys/__init__.py +7 -4
- edsl/surveys/dag/__init__.py +2 -0
- edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
- edsl/surveys/{DAG.py → dag/dag.py} +13 -10
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
- edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
- edsl/surveys/memory/__init__.py +3 -0
- edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
- edsl/surveys/rules/__init__.py +3 -0
- edsl/surveys/{Rule.py → rules/rule.py} +103 -43
- edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
- edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
- edsl/surveys/survey.py +1743 -0
- edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
- edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
- edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
- edsl/tasks/__init__.py +32 -0
- edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
- edsl/tasks/task_creators.py +135 -0
- edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
- edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
- edsl/tasks/task_status_log.py +85 -0
- edsl/tokens/__init__.py +2 -0
- edsl/tokens/interview_token_usage.py +53 -0
- edsl/utilities/PrettyList.py +1 -1
- edsl/utilities/SystemInfo.py +25 -22
- edsl/utilities/__init__.py +29 -21
- edsl/utilities/gcp_bucket/__init__.py +2 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
- edsl/utilities/interface.py +44 -536
- edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
- edsl/utilities/repair_functions.py +1 -1
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/METADATA +1 -1
- edsl-0.1.48.dist-info/RECORD +347 -0
- edsl/Base.py +0 -493
- edsl/BaseDiff.py +0 -260
- edsl/agents/InvigilatorBase.py +0 -260
- edsl/agents/PromptConstructor.py +0 -318
- edsl/coop/PriceFetcher.py +0 -54
- edsl/data/Cache.py +0 -582
- edsl/data/CacheEntry.py +0 -238
- edsl/data/SQLiteDict.py +0 -292
- edsl/data/__init__.py +0 -5
- edsl/data/orm.py +0 -10
- edsl/exceptions/cache.py +0 -5
- edsl/exceptions/coop.py +0 -14
- edsl/exceptions/data.py +0 -14
- edsl/exceptions/scenarios.py +0 -29
- edsl/jobs/Answers.py +0 -43
- edsl/jobs/JobsPrompts.py +0 -354
- edsl/jobs/buckets/BucketCollection.py +0 -134
- edsl/jobs/buckets/ModelBuckets.py +0 -65
- edsl/jobs/buckets/TokenBucket.py +0 -283
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/interviews/Interview.py +0 -395
- edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
- edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
- edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
- edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
- edsl/jobs/tasks/TaskCreators.py +0 -64
- edsl/jobs/tasks/TaskStatusLog.py +0 -23
- edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
- edsl/language_models/LanguageModel.py +0 -635
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/models.py +0 -137
- edsl/questions/QuestionBase.py +0 -544
- edsl/questions/QuestionFreeText.py +0 -130
- edsl/questions/derived/QuestionLikertFive.py +0 -76
- edsl/results/ResultsExportMixin.py +0 -45
- edsl/results/TextEditor.py +0 -50
- edsl/results/results_fetch_mixin.py +0 -33
- edsl/results/results_tools_mixin.py +0 -98
- edsl/scenarios/DocumentChunker.py +0 -104
- edsl/scenarios/Scenario.py +0 -548
- edsl/scenarios/ScenarioHtmlMixin.py +0 -65
- edsl/scenarios/ScenarioListExportMixin.py +0 -45
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/shared.py +0 -1
- edsl/surveys/Survey.py +0 -1301
- edsl/surveys/SurveyQualtricsImport.py +0 -284
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/tools/__init__.py +0 -1
- edsl/tools/clusters.py +0 -192
- edsl/tools/embeddings.py +0 -27
- edsl/tools/embeddings_plotting.py +0 -118
- edsl/tools/plotting.py +0 -112
- edsl/tools/summarize.py +0 -18
- edsl/utilities/data/Registry.py +0 -6
- edsl/utilities/data/__init__.py +0 -1
- edsl/utilities/data/scooter_results.json +0 -1
- edsl-0.1.47.dist-info/RECORD +0 -354
- /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
- /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
- /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
- /edsl/{results → dataset/display}/table_data_class.py +0 -0
- /edsl/{results → dataset/display}/table_display.css +0 -0
- /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
- /edsl/{results → dataset}/tree_explore.py +0 -0
- /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
- /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
- /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
- /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
- /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
- /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
- /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
- /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
- /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
- /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
- /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
- /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
- /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
- /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
- /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
edsl/base/base_class.py
ADDED
@@ -0,0 +1,1303 @@
|
|
1
|
+
"""Base class for all classes in the package.
|
2
|
+
|
3
|
+
This module provides the foundation for all classes in the EDSL framework, implementing core
|
4
|
+
functionality such as serialization, persistence, rich representation, and object comparison.
|
5
|
+
The Base class combines several mixins that provide different aspects of functionality:
|
6
|
+
- RepresentationMixin: Handles object display and visualization
|
7
|
+
- PersistenceMixin: Manages saving/loading objects and cloud operations
|
8
|
+
- DiffMethodsMixin: Enables object comparison and differencing
|
9
|
+
- HashingMixin: Provides consistent hashing and equality operations
|
10
|
+
|
11
|
+
Classes inheriting from Base get a rich set of capabilities "for free" including
|
12
|
+
JSON/YAML serialization, file persistence, pretty printing, and object comparison.
|
13
|
+
"""
|
14
|
+
|
15
|
+
from abc import ABC, abstractmethod, ABCMeta
|
16
|
+
import gzip
|
17
|
+
import json
|
18
|
+
from typing import Any, Optional, Union
|
19
|
+
from uuid import UUID
|
20
|
+
import difflib
|
21
|
+
import json
|
22
|
+
from typing import Any, Dict, Tuple
|
23
|
+
from collections import UserList
|
24
|
+
import inspect
|
25
|
+
|
26
|
+
class BaseException(Exception):
|
27
|
+
"""Base exception class for all EDSL exceptions.
|
28
|
+
|
29
|
+
This class extends the standard Python Exception class to provide more helpful error messages
|
30
|
+
by including links to relevant documentation and example notebooks when available.
|
31
|
+
|
32
|
+
Attributes:
|
33
|
+
relevant_doc: URL to documentation explaining this type of exception
|
34
|
+
relevant_notebook: Optional URL to a notebook with usage examples
|
35
|
+
"""
|
36
|
+
relevant_doc = "https://docs.expectedparrot.com/"
|
37
|
+
|
38
|
+
def __init__(self, message, *, show_docs=True):
|
39
|
+
"""Initialize a new BaseException with formatted error message.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
message: The primary error message
|
43
|
+
show_docs: If True, append documentation links to the error message
|
44
|
+
"""
|
45
|
+
# Format main error message
|
46
|
+
formatted_message = [message.strip()]
|
47
|
+
|
48
|
+
# Add documentation links if requested
|
49
|
+
if show_docs:
|
50
|
+
if hasattr(self, "relevant_doc"):
|
51
|
+
formatted_message.append(
|
52
|
+
f"\nFor more information, see:\n{self.relevant_doc}"
|
53
|
+
)
|
54
|
+
if hasattr(self, "relevant_notebook"):
|
55
|
+
formatted_message.append(
|
56
|
+
f"\nFor a usage example, see:\n{self.relevant_notebook}"
|
57
|
+
)
|
58
|
+
|
59
|
+
# Join with double newlines for clear separation
|
60
|
+
final_message = "\n\n".join(formatted_message)
|
61
|
+
super().__init__(final_message)
|
62
|
+
|
63
|
+
|
64
|
+
class DisplayJSON:
|
65
|
+
"""Display a dictionary as JSON."""
|
66
|
+
|
67
|
+
def __init__(self, input_dict: dict):
|
68
|
+
self.text = json.dumps(input_dict, indent=4)
|
69
|
+
|
70
|
+
def __repr__(self):
|
71
|
+
return self.text
|
72
|
+
|
73
|
+
|
74
|
+
class DisplayYAML:
|
75
|
+
"""Display a dictionary as YAML."""
|
76
|
+
|
77
|
+
def __init__(self, input_dict: dict):
|
78
|
+
import yaml
|
79
|
+
|
80
|
+
self.text = yaml.dump(input_dict)
|
81
|
+
|
82
|
+
def __repr__(self):
|
83
|
+
return self.text
|
84
|
+
|
85
|
+
|
86
|
+
class PersistenceMixin:
|
87
|
+
"""Mixin for saving and loading objects to and from files.
|
88
|
+
|
89
|
+
This mixin provides methods for serializing objects to various formats (JSON, YAML),
|
90
|
+
saving to and loading from files, and interacting with cloud storage. It enables
|
91
|
+
persistence operations like duplicating objects and uploading/downloading from the
|
92
|
+
EDSL cooperative platform.
|
93
|
+
"""
|
94
|
+
|
95
|
+
def duplicate(self, add_edsl_version=False):
|
96
|
+
"""Create and return a deep copy of the object.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
add_edsl_version: Whether to include EDSL version information in the duplicated object
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
A new instance of the same class with identical properties
|
103
|
+
"""
|
104
|
+
return self.from_dict(self.to_dict(add_edsl_version=False))
|
105
|
+
|
106
|
+
@classmethod
|
107
|
+
def help(cls):
|
108
|
+
"""Display the class documentation string.
|
109
|
+
|
110
|
+
This is a convenience method to quickly access the docstring of the class.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
None, but prints the class docstring to stdout
|
114
|
+
"""
|
115
|
+
print(cls.__doc__)
|
116
|
+
|
117
|
+
def push(
|
118
|
+
self,
|
119
|
+
description: Optional[str] = None,
|
120
|
+
alias: Optional[str] = None,
|
121
|
+
visibility: Optional[str] = "unlisted",
|
122
|
+
expected_parrot_url: Optional[str] = None,
|
123
|
+
):
|
124
|
+
"""Upload this object to the EDSL cooperative platform.
|
125
|
+
|
126
|
+
This method serializes the object and posts it to the EDSL coop service,
|
127
|
+
making it accessible to others or for your own use across sessions.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
description: Optional text description of the object
|
131
|
+
alias: Optional human-readable identifier for the object
|
132
|
+
visibility: Access level setting ("private", "unlisted", or "public")
|
133
|
+
expected_parrot_url: Optional custom URL for the coop service
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
The response from the coop service containing the object's unique identifier
|
137
|
+
"""
|
138
|
+
from edsl.coop import Coop
|
139
|
+
|
140
|
+
c = Coop(url=expected_parrot_url)
|
141
|
+
return c.create(self, description, alias, visibility)
|
142
|
+
|
143
|
+
def to_yaml(self, add_edsl_version=False, filename: str = None) -> Union[str, None]:
|
144
|
+
"""Convert the object to YAML format.
|
145
|
+
|
146
|
+
Serializes the object to YAML format and optionally writes it to a file.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
add_edsl_version: Whether to include EDSL version information
|
150
|
+
filename: If provided, write the YAML to this file path
|
151
|
+
|
152
|
+
Returns:
|
153
|
+
str: The YAML string representation if no filename is provided
|
154
|
+
None: If written to file
|
155
|
+
"""
|
156
|
+
import yaml
|
157
|
+
|
158
|
+
output = yaml.dump(self.to_dict(add_edsl_version=add_edsl_version))
|
159
|
+
if not filename:
|
160
|
+
return output
|
161
|
+
|
162
|
+
with open(filename, "w") as f:
|
163
|
+
f.write(output)
|
164
|
+
|
165
|
+
@classmethod
|
166
|
+
def from_yaml(cls, yaml_str: Optional[str] = None, filename: Optional[str] = None):
|
167
|
+
"""Create an instance from YAML data.
|
168
|
+
|
169
|
+
Deserializes a YAML string or file into a new instance of the class.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
yaml_str: YAML string containing object data
|
173
|
+
filename: Path to a YAML file containing object data
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
A new instance of the class populated with the deserialized data
|
177
|
+
|
178
|
+
Raises:
|
179
|
+
ValueError: If neither yaml_str nor filename is provided
|
180
|
+
"""
|
181
|
+
if yaml_str is None and filename is not None:
|
182
|
+
with open(filename, "r") as f:
|
183
|
+
yaml_str = f.read()
|
184
|
+
return cls.from_yaml(yaml_str=yaml_str)
|
185
|
+
elif yaml_str and filename is None:
|
186
|
+
import yaml
|
187
|
+
|
188
|
+
d = yaml.load(yaml_str, Loader=yaml.FullLoader)
|
189
|
+
return cls.from_dict(d)
|
190
|
+
else:
|
191
|
+
raise ValueError("Either yaml_str or filename must be provided.")
|
192
|
+
|
193
|
+
def create_download_link(self):
|
194
|
+
"""Generate a downloadable link for this object.
|
195
|
+
|
196
|
+
Creates a temporary file containing the serialized object and generates
|
197
|
+
a download link that can be shared with others.
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
str: A URL that can be used to download the object
|
201
|
+
"""
|
202
|
+
from tempfile import NamedTemporaryFile
|
203
|
+
from edsl.scenarios import FileStore
|
204
|
+
|
205
|
+
with NamedTemporaryFile(suffix=".json.gz") as f:
|
206
|
+
self.save(f.name)
|
207
|
+
print(f.name)
|
208
|
+
fs = FileStore(path=f.name)
|
209
|
+
return fs.create_link()
|
210
|
+
|
211
|
+
@classmethod
|
212
|
+
def pull(
|
213
|
+
cls,
|
214
|
+
url_or_uuid: Optional[Union[str, UUID]] = None,
|
215
|
+
):
|
216
|
+
"""Pull the object from coop.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
url_or_uuid: Either a UUID string or a URL pointing to the object
|
220
|
+
"""
|
221
|
+
from edsl.coop import Coop
|
222
|
+
from edsl.coop import ObjectRegistry
|
223
|
+
|
224
|
+
object_type = ObjectRegistry.get_object_type_by_edsl_class(cls)
|
225
|
+
coop = Coop()
|
226
|
+
|
227
|
+
return coop.get(url_or_uuid, expected_object_type=object_type)
|
228
|
+
|
229
|
+
@classmethod
|
230
|
+
def delete(cls, url_or_uuid: Union[str, UUID]) -> None:
|
231
|
+
"""Delete the object from coop."""
|
232
|
+
from edsl.coop import Coop
|
233
|
+
|
234
|
+
coop = Coop()
|
235
|
+
|
236
|
+
return coop.delete(url_or_uuid)
|
237
|
+
|
238
|
+
@classmethod
|
239
|
+
def patch_cls(
|
240
|
+
cls,
|
241
|
+
url_or_uuid: Union[str, UUID],
|
242
|
+
description: Optional[str] = None,
|
243
|
+
value: Optional[Any] = None,
|
244
|
+
visibility: Optional[str] = None,
|
245
|
+
):
|
246
|
+
"""
|
247
|
+
Patch an uploaded object's attributes (class method version).
|
248
|
+
- `description` changes the description of the object on Coop
|
249
|
+
- `value` changes the value of the object on Coop. **has to be an EDSL object**
|
250
|
+
- `visibility` changes the visibility of the object on Coop
|
251
|
+
"""
|
252
|
+
from edsl.coop import Coop
|
253
|
+
|
254
|
+
coop = Coop()
|
255
|
+
|
256
|
+
return coop.patch(
|
257
|
+
url_or_uuid=url_or_uuid,
|
258
|
+
description=description,
|
259
|
+
value=value,
|
260
|
+
visibility=visibility,
|
261
|
+
)
|
262
|
+
|
263
|
+
class ClassOrInstanceMethod:
|
264
|
+
"""Descriptor that allows a method to be called as both a class method and an instance method."""
|
265
|
+
|
266
|
+
def __init__(self, func):
|
267
|
+
self.func = func
|
268
|
+
|
269
|
+
def __get__(self, obj, objtype=None):
|
270
|
+
if obj is None:
|
271
|
+
# Called as a class method
|
272
|
+
def wrapper(*args, **kwargs):
|
273
|
+
return self.func(objtype, *args, **kwargs)
|
274
|
+
|
275
|
+
return wrapper
|
276
|
+
else:
|
277
|
+
# Called as an instance method
|
278
|
+
def wrapper(*args, **kwargs):
|
279
|
+
return self.func(obj, *args, **kwargs)
|
280
|
+
|
281
|
+
return wrapper
|
282
|
+
|
283
|
+
@ClassOrInstanceMethod
|
284
|
+
def patch(
|
285
|
+
self_or_cls,
|
286
|
+
url_or_uuid: Union[str, UUID],
|
287
|
+
description: Optional[str] = None,
|
288
|
+
value: Optional[Any] = None,
|
289
|
+
visibility: Optional[str] = None,
|
290
|
+
):
|
291
|
+
"""
|
292
|
+
Patch an uploaded object's attributes.
|
293
|
+
|
294
|
+
When called as a class method:
|
295
|
+
- Requires explicit `value` parameter
|
296
|
+
|
297
|
+
When called as an instance method:
|
298
|
+
- Uses the instance itself as the `value` parameter
|
299
|
+
|
300
|
+
Parameters:
|
301
|
+
- `id_or_url`: ID or URL of the object to patch
|
302
|
+
- `description`: changes the description of the object on Coop
|
303
|
+
- `value`: changes the value of the object on Coop (required for class method)
|
304
|
+
- `visibility`: changes the visibility of the object on Coop
|
305
|
+
"""
|
306
|
+
|
307
|
+
# Check if this is being called as a class method
|
308
|
+
if isinstance(self_or_cls, type):
|
309
|
+
# This is a class method call
|
310
|
+
cls = self_or_cls
|
311
|
+
return cls.patch_cls(
|
312
|
+
url_or_uuid=url_or_uuid,
|
313
|
+
description=description,
|
314
|
+
value=value,
|
315
|
+
visibility=visibility,
|
316
|
+
)
|
317
|
+
else:
|
318
|
+
# This is an instance method call
|
319
|
+
instance = self_or_cls
|
320
|
+
cls_type = instance.__class__
|
321
|
+
|
322
|
+
# Use the instance as the value if not explicitly provided
|
323
|
+
if value is None:
|
324
|
+
value = instance
|
325
|
+
else:
|
326
|
+
pass
|
327
|
+
|
328
|
+
return cls_type.patch_cls(
|
329
|
+
url_or_uuid=url_or_uuid,
|
330
|
+
description=description,
|
331
|
+
value=value,
|
332
|
+
visibility=visibility,
|
333
|
+
)
|
334
|
+
|
335
|
+
@classmethod
|
336
|
+
def search(cls, query):
|
337
|
+
"""Search for objects on coop."""
|
338
|
+
from edsl.coop import Coop
|
339
|
+
|
340
|
+
c = Coop()
|
341
|
+
return c.search(cls, query)
|
342
|
+
|
343
|
+
def store(self, d: dict, key_name: Optional[str] = None):
|
344
|
+
if key_name is None:
|
345
|
+
index = len(d)
|
346
|
+
else:
|
347
|
+
index = key_name
|
348
|
+
d[index] = self
|
349
|
+
|
350
|
+
def save(self, filename, compress=True):
|
351
|
+
"""Save the object to a file as JSON with optional compression.
|
352
|
+
|
353
|
+
Serializes the object to JSON and writes it to the specified file.
|
354
|
+
By default, the file will be compressed using gzip. File extensions
|
355
|
+
are handled automatically.
|
356
|
+
|
357
|
+
Args:
|
358
|
+
filename: Path where the file should be saved
|
359
|
+
compress: If True, compress the file using gzip (default: True)
|
360
|
+
|
361
|
+
Returns:
|
362
|
+
None
|
363
|
+
|
364
|
+
Examples:
|
365
|
+
>>> obj.save("my_object.json.gz") # Compressed
|
366
|
+
>>> obj.save("my_object.json", compress=False) # Uncompressed
|
367
|
+
"""
|
368
|
+
if filename.endswith("json.gz"):
|
369
|
+
import warnings
|
370
|
+
|
371
|
+
filename = filename[:-8]
|
372
|
+
if filename.endswith("json"):
|
373
|
+
filename = filename[:-5]
|
374
|
+
|
375
|
+
if compress:
|
376
|
+
full_file_name = filename + ".json.gz"
|
377
|
+
with gzip.open(full_file_name, "wb") as f:
|
378
|
+
f.write(json.dumps(self.to_dict()).encode("utf-8"))
|
379
|
+
else:
|
380
|
+
full_file_name = filename + ".json"
|
381
|
+
with open(filename + ".json", "w") as f:
|
382
|
+
f.write(json.dumps(self.to_dict()))
|
383
|
+
|
384
|
+
print("Saved to", full_file_name)
|
385
|
+
|
386
|
+
@staticmethod
|
387
|
+
def open_compressed_file(filename):
|
388
|
+
"""Read and parse a compressed JSON file.
|
389
|
+
|
390
|
+
Args:
|
391
|
+
filename: Path to a gzipped JSON file
|
392
|
+
|
393
|
+
Returns:
|
394
|
+
dict: The parsed JSON content
|
395
|
+
"""
|
396
|
+
with gzip.open(filename, "rb") as f:
|
397
|
+
file_contents = f.read()
|
398
|
+
file_contents_decoded = file_contents.decode("utf-8")
|
399
|
+
d = json.loads(file_contents_decoded)
|
400
|
+
return d
|
401
|
+
|
402
|
+
@staticmethod
|
403
|
+
def open_regular_file(filename):
|
404
|
+
"""Read and parse an uncompressed JSON file.
|
405
|
+
|
406
|
+
Args:
|
407
|
+
filename: Path to a JSON file
|
408
|
+
|
409
|
+
Returns:
|
410
|
+
dict: The parsed JSON content
|
411
|
+
"""
|
412
|
+
with open(filename, "r") as f:
|
413
|
+
d = json.loads(f.read())
|
414
|
+
return d
|
415
|
+
|
416
|
+
@classmethod
|
417
|
+
def load(cls, filename):
|
418
|
+
"""Load the object from a JSON file (compressed or uncompressed).
|
419
|
+
|
420
|
+
This method deserializes an object from a file, automatically detecting
|
421
|
+
whether the file is compressed with gzip or not.
|
422
|
+
|
423
|
+
Args:
|
424
|
+
filename: Path to the file to load
|
425
|
+
|
426
|
+
Returns:
|
427
|
+
An instance of the class populated with data from the file
|
428
|
+
|
429
|
+
Raises:
|
430
|
+
Various exceptions may be raised if the file doesn't exist or contains invalid data
|
431
|
+
"""
|
432
|
+
if filename.endswith("json.gz"):
|
433
|
+
d = cls.open_compressed_file(filename)
|
434
|
+
elif filename.endswith("json"):
|
435
|
+
d = cls.open_regular_file(filename)
|
436
|
+
else:
|
437
|
+
try:
|
438
|
+
d = cls.open_compressed_file(filename + ".json.gz")
|
439
|
+
except:
|
440
|
+
d = cls.open_regular_file(filename + ".json")
|
441
|
+
# finally:
|
442
|
+
# raise ValueError("File must be a json or json.gz file")
|
443
|
+
|
444
|
+
return cls.from_dict(d)
|
445
|
+
|
446
|
+
|
447
|
+
class RegisterSubclassesMeta(ABCMeta):
|
448
|
+
"""Metaclass for automatically registering all subclasses.
|
449
|
+
|
450
|
+
This metaclass maintains a registry of all classes that inherit from Base,
|
451
|
+
allowing for dynamic discovery of available classes and capabilities like
|
452
|
+
automatic deserialization. When a new class is defined with Base as its
|
453
|
+
parent, this metaclass automatically adds it to the registry.
|
454
|
+
"""
|
455
|
+
|
456
|
+
_registry = {}
|
457
|
+
|
458
|
+
def __init__(cls, name, bases, nmspc):
|
459
|
+
"""Register the class in the registry upon creation.
|
460
|
+
|
461
|
+
Args:
|
462
|
+
name: The name of the class being created
|
463
|
+
bases: The base classes of the class being created
|
464
|
+
nmspc: The namespace of the class being created
|
465
|
+
"""
|
466
|
+
super(RegisterSubclassesMeta, cls).__init__(name, bases, nmspc)
|
467
|
+
if cls.__name__ != "Base":
|
468
|
+
RegisterSubclassesMeta._registry[cls.__name__] = cls
|
469
|
+
|
470
|
+
@staticmethod
|
471
|
+
def get_registry(exclude_classes: Optional[list] = None):
|
472
|
+
"""Get the registry of all registered subclasses.
|
473
|
+
|
474
|
+
Args:
|
475
|
+
exclude_classes: Optional list of class names to exclude from the result
|
476
|
+
|
477
|
+
Returns:
|
478
|
+
dict: A dictionary mapping class names to class objects
|
479
|
+
"""
|
480
|
+
if exclude_classes is None:
|
481
|
+
exclude_classes = []
|
482
|
+
return {
|
483
|
+
k: v
|
484
|
+
for k, v in dict(RegisterSubclassesMeta._registry).items()
|
485
|
+
if k not in exclude_classes
|
486
|
+
}
|
487
|
+
|
488
|
+
|
489
|
+
class DiffMethodsMixin:
|
490
|
+
"""Mixin that adds the ability to compute differences between objects.
|
491
|
+
|
492
|
+
This mixin provides operator overloads that enable convenient comparison and
|
493
|
+
differencing between objects of the same class.
|
494
|
+
"""
|
495
|
+
|
496
|
+
def __sub__(self, other):
|
497
|
+
"""Calculate the difference between this object and another.
|
498
|
+
|
499
|
+
This overloads the subtraction operator (-) to provide an intuitive way
|
500
|
+
to compare objects and find their differences.
|
501
|
+
|
502
|
+
Args:
|
503
|
+
other: Another object to compare against this one
|
504
|
+
|
505
|
+
Returns:
|
506
|
+
BaseDiff: An object representing the differences between the two objects
|
507
|
+
"""
|
508
|
+
from edsl.base import BaseDiff
|
509
|
+
|
510
|
+
return BaseDiff(self, other)
|
511
|
+
|
512
|
+
|
513
|
+
def is_iterable(obj):
|
514
|
+
"""Check if an object is iterable.
|
515
|
+
|
516
|
+
Args:
|
517
|
+
obj: The object to check
|
518
|
+
|
519
|
+
Returns:
|
520
|
+
bool: True if the object is iterable, False otherwise
|
521
|
+
"""
|
522
|
+
try:
|
523
|
+
iter(obj)
|
524
|
+
except TypeError:
|
525
|
+
return False
|
526
|
+
return True
|
527
|
+
|
528
|
+
|
529
|
+
class RepresentationMixin:
|
530
|
+
"""Mixin that provides rich display and representation capabilities.
|
531
|
+
|
532
|
+
This mixin enhances objects with methods for displaying their contents in various
|
533
|
+
formats including JSON, HTML tables, and rich terminal output. It improves the
|
534
|
+
user experience when working with EDSL objects in notebooks and terminals.
|
535
|
+
"""
|
536
|
+
|
537
|
+
def json(self):
|
538
|
+
"""Get a parsed JSON representation of this object.
|
539
|
+
|
540
|
+
Returns:
|
541
|
+
dict: The object's data as a Python dictionary
|
542
|
+
"""
|
543
|
+
return json.loads(json.dumps(self.to_dict(add_edsl_version=False)))
|
544
|
+
|
545
|
+
def to_dataset(self):
|
546
|
+
"""Convert this object to a Dataset for advanced data operations.
|
547
|
+
|
548
|
+
Returns:
|
549
|
+
Dataset: A Dataset object containing this object's data
|
550
|
+
"""
|
551
|
+
from edsl.dataset import Dataset
|
552
|
+
|
553
|
+
return Dataset.from_edsl_object(self)
|
554
|
+
|
555
|
+
def view(self):
|
556
|
+
"""Display an interactive visualization of this object.
|
557
|
+
|
558
|
+
Returns:
|
559
|
+
The result of the dataset's view method
|
560
|
+
"""
|
561
|
+
return self.to_dataset().view()
|
562
|
+
|
563
|
+
# def print(self, format="rich"):
|
564
|
+
# return self.to_dataset().table()
|
565
|
+
|
566
|
+
def display_dict(self):
|
567
|
+
"""Create a flattened dictionary representation for display purposes.
|
568
|
+
|
569
|
+
This method creates a flattened view of nested structures using colon notation
|
570
|
+
in keys to represent hierarchy.
|
571
|
+
|
572
|
+
Returns:
|
573
|
+
dict: A flattened dictionary suitable for display
|
574
|
+
"""
|
575
|
+
display_dict = {}
|
576
|
+
d = self.to_dict(add_edsl_version=False)
|
577
|
+
for key, value in d.items():
|
578
|
+
if isinstance(value, dict):
|
579
|
+
for k, v in value.items():
|
580
|
+
display_dict[f"{key}:{k}"] = v
|
581
|
+
elif isinstance(value, list):
|
582
|
+
for i, v in enumerate(value):
|
583
|
+
display_dict[f"{key}:{i}"] = v
|
584
|
+
else:
|
585
|
+
display_dict[key] = value
|
586
|
+
return display_dict
|
587
|
+
|
588
|
+
def print(self, format="rich"):
|
589
|
+
"""Print a formatted table representation of this object.
|
590
|
+
|
591
|
+
Args:
|
592
|
+
format: The output format (currently only 'rich' is supported)
|
593
|
+
|
594
|
+
Returns:
|
595
|
+
None, but prints a formatted table to the console
|
596
|
+
"""
|
597
|
+
from rich.table import Table
|
598
|
+
from rich.console import Console
|
599
|
+
|
600
|
+
table = Table(title=self.__class__.__name__)
|
601
|
+
table.add_column("Key", style="bold")
|
602
|
+
table.add_column("Value", style="bold")
|
603
|
+
|
604
|
+
for key, value in self.display_dict().items():
|
605
|
+
table.add_row(key, str(value))
|
606
|
+
|
607
|
+
console = Console(record=True)
|
608
|
+
console.print(table)
|
609
|
+
|
610
|
+
def _repr_html_(self):
|
611
|
+
"""Generate an HTML representation for Jupyter notebooks.
|
612
|
+
|
613
|
+
This method is automatically called by Jupyter to render the object
|
614
|
+
as HTML in notebook cells.
|
615
|
+
|
616
|
+
Returns:
|
617
|
+
str: HTML representation of the object
|
618
|
+
"""
|
619
|
+
from edsl.dataset.display.table_display import TableDisplay
|
620
|
+
|
621
|
+
if hasattr(self, "_summary"):
|
622
|
+
summary_dict = self._summary()
|
623
|
+
summary_line = "".join([f" {k}: {v};" for k, v in summary_dict.items()])
|
624
|
+
class_name = self.__class__.__name__
|
625
|
+
docs = getattr(self, "__documentation__", "")
|
626
|
+
return (
|
627
|
+
"<p>"
|
628
|
+
+ f"<a href='{docs}'>{class_name}</a>"
|
629
|
+
+ summary_line
|
630
|
+
+ "</p>"
|
631
|
+
+ self.table()._repr_html_()
|
632
|
+
)
|
633
|
+
else:
|
634
|
+
class_name = self.__class__.__name__
|
635
|
+
documentation = getattr(self, "__documentation__", "")
|
636
|
+
summary_line = "<p>" + f"<a href='{documentation}'>{class_name}</a>" + "</p>"
|
637
|
+
display_dict = self.display_dict()
|
638
|
+
return (
|
639
|
+
summary_line
|
640
|
+
+ TableDisplay.from_dictionary_wide(display_dict)._repr_html_()
|
641
|
+
)
|
642
|
+
|
643
|
+
def __str__(self):
|
644
|
+
"""Return the string representation of the object.
|
645
|
+
|
646
|
+
Returns:
|
647
|
+
str: String representation of the object
|
648
|
+
"""
|
649
|
+
return self.__repr__()
|
650
|
+
|
651
|
+
|
652
|
+
class HashingMixin:
|
653
|
+
"""Mixin that provides consistent hashing and equality operations.
|
654
|
+
|
655
|
+
This mixin implements __hash__ and __eq__ methods to enable using EDSL objects
|
656
|
+
in sets and as dictionary keys. The hash is based on the object's serialized content,
|
657
|
+
so two objects with identical content will be considered equal.
|
658
|
+
"""
|
659
|
+
|
660
|
+
def __hash__(self) -> int:
|
661
|
+
"""Generate a hash value for this object based on its content.
|
662
|
+
|
663
|
+
The hash is computed from the serialized dictionary representation of the object,
|
664
|
+
excluding any version information.
|
665
|
+
|
666
|
+
Returns:
|
667
|
+
int: A hash value for the object
|
668
|
+
"""
|
669
|
+
from edsl.utilities.utilities import dict_hash
|
670
|
+
|
671
|
+
return dict_hash(self.to_dict(add_edsl_version=False))
|
672
|
+
|
673
|
+
def __eq__(self, other):
|
674
|
+
"""Compare this object with another for equality.
|
675
|
+
|
676
|
+
Two objects are considered equal if they have the same hash value,
|
677
|
+
which means they have identical content.
|
678
|
+
|
679
|
+
Args:
|
680
|
+
other: Another object to compare with this one
|
681
|
+
|
682
|
+
Returns:
|
683
|
+
bool: True if the objects are equal, False otherwise
|
684
|
+
"""
|
685
|
+
return hash(self) == hash(other)
|
686
|
+
|
687
|
+
|
688
|
+
class Base(
|
689
|
+
RepresentationMixin,
|
690
|
+
PersistenceMixin,
|
691
|
+
DiffMethodsMixin,
|
692
|
+
HashingMixin,
|
693
|
+
ABC,
|
694
|
+
metaclass=RegisterSubclassesMeta,
|
695
|
+
):
|
696
|
+
"""Base class for all classes in the EDSL package.
|
697
|
+
|
698
|
+
This abstract base class combines several mixins to provide a rich set of functionality
|
699
|
+
to all EDSL objects. It defines the core interface that all EDSL objects must implement,
|
700
|
+
including serialization, deserialization, and code generation.
|
701
|
+
|
702
|
+
All EDSL classes should inherit from this class to ensure consistent behavior
|
703
|
+
and capabilities across the framework.
|
704
|
+
"""
|
705
|
+
|
706
|
+
def keys(self):
|
707
|
+
"""Get the key names in the object's dictionary representation.
|
708
|
+
|
709
|
+
This method returns all the keys in the serialized form of the object,
|
710
|
+
excluding metadata keys like version information.
|
711
|
+
|
712
|
+
Returns:
|
713
|
+
list: A list of key names
|
714
|
+
"""
|
715
|
+
_keys = list(self.to_dict().keys())
|
716
|
+
if "edsl_version" in _keys:
|
717
|
+
_keys.remove("edsl_version")
|
718
|
+
if "edsl_class_name" in _keys:
|
719
|
+
_keys.remove("edsl_class_name")
|
720
|
+
return _keys
|
721
|
+
|
722
|
+
def values(self):
|
723
|
+
"""Get the values in the object's dictionary representation.
|
724
|
+
|
725
|
+
Returns:
|
726
|
+
set: A set containing all the values in the object
|
727
|
+
"""
|
728
|
+
data = self.to_dict()
|
729
|
+
keys = self.keys()
|
730
|
+
return {data[key] for key in keys}
|
731
|
+
|
732
|
+
@abstractmethod
|
733
|
+
def example():
|
734
|
+
"""Create an example instance of this class.
|
735
|
+
|
736
|
+
This method should be implemented by all subclasses to provide
|
737
|
+
a convenient way to create example objects for testing and demonstration.
|
738
|
+
|
739
|
+
Returns:
|
740
|
+
An instance of the class with sample data
|
741
|
+
"""
|
742
|
+
raise NotImplementedError("This method is not implemented yet.")
|
743
|
+
|
744
|
+
def json(self):
|
745
|
+
"""Get a formatted JSON representation of this object.
|
746
|
+
|
747
|
+
Returns:
|
748
|
+
DisplayJSON: A displayable JSON representation
|
749
|
+
"""
|
750
|
+
return DisplayJSON(self.to_dict(add_edsl_version=False))
|
751
|
+
|
752
|
+
def yaml(self):
|
753
|
+
"""Get a formatted YAML representation of this object.
|
754
|
+
|
755
|
+
Returns:
|
756
|
+
DisplayYAML: A displayable YAML representation
|
757
|
+
"""
|
758
|
+
import yaml
|
759
|
+
return DisplayYAML(self.to_dict(add_edsl_version=False))
|
760
|
+
|
761
|
+
|
762
|
+
@abstractmethod
|
763
|
+
def to_dict():
|
764
|
+
"""Serialize this object to a dictionary.
|
765
|
+
|
766
|
+
This method must be implemented by all subclasses to provide a
|
767
|
+
standard way to serialize objects to dictionaries. The dictionary
|
768
|
+
should contain all the data needed to reconstruct the object.
|
769
|
+
|
770
|
+
Returns:
|
771
|
+
dict: A dictionary representation of the object
|
772
|
+
"""
|
773
|
+
raise NotImplementedError("This method is not implemented yet.")
|
774
|
+
|
775
|
+
def to_json(self):
|
776
|
+
"""Serialize this object to a JSON string.
|
777
|
+
|
778
|
+
Returns:
|
779
|
+
str: A JSON string representation of the object
|
780
|
+
"""
|
781
|
+
return json.dumps(self.to_dict())
|
782
|
+
|
783
|
+
def store(self, d: dict, key_name: Optional[str] = None):
|
784
|
+
"""Store this object in a dictionary with an optional key.
|
785
|
+
|
786
|
+
Args:
|
787
|
+
d: The dictionary in which to store the object
|
788
|
+
key_name: Optional key to use (defaults to the length of the dictionary)
|
789
|
+
|
790
|
+
Returns:
|
791
|
+
None
|
792
|
+
"""
|
793
|
+
if key_name is None:
|
794
|
+
index = len(d)
|
795
|
+
else:
|
796
|
+
index = key_name
|
797
|
+
d[index] = self
|
798
|
+
|
799
|
+
@abstractmethod
|
800
|
+
def from_dict():
|
801
|
+
"""Create an instance from a dictionary.
|
802
|
+
|
803
|
+
This class method must be implemented by all subclasses to provide a
|
804
|
+
standard way to deserialize objects from dictionaries.
|
805
|
+
|
806
|
+
Returns:
|
807
|
+
An instance of the class populated with data from the dictionary
|
808
|
+
"""
|
809
|
+
raise NotImplementedError("This method is not implemented yet.")
|
810
|
+
|
811
|
+
@abstractmethod
|
812
|
+
def code():
|
813
|
+
"""Generate Python code that recreates this object.
|
814
|
+
|
815
|
+
This method must be implemented by all subclasses to provide a way to
|
816
|
+
generate executable Python code that can recreate the object.
|
817
|
+
|
818
|
+
Returns:
|
819
|
+
str: Python code that, when executed, creates an equivalent object
|
820
|
+
"""
|
821
|
+
raise NotImplementedError("This method is not implemented yet.")
|
822
|
+
|
823
|
+
def show_methods(self, show_docstrings=True):
|
824
|
+
"""Display all public methods available on this object.
|
825
|
+
|
826
|
+
This utility method helps explore the capabilities of an object by listing
|
827
|
+
all its public methods and optionally their documentation.
|
828
|
+
|
829
|
+
Args:
|
830
|
+
show_docstrings: If True, print method names with docstrings;
|
831
|
+
if False, return the list of method names
|
832
|
+
|
833
|
+
Returns:
|
834
|
+
None or list: If show_docstrings is True, prints methods and returns None.
|
835
|
+
If show_docstrings is False, returns a list of method names.
|
836
|
+
"""
|
837
|
+
public_methods_with_docstrings = [
|
838
|
+
(method, getattr(self, method).__doc__)
|
839
|
+
for method in dir(self)
|
840
|
+
if callable(getattr(self, method)) and not method.startswith("_")
|
841
|
+
]
|
842
|
+
if show_docstrings:
|
843
|
+
for method, documentation in public_methods_with_docstrings:
|
844
|
+
print(f"{method}: {documentation}")
|
845
|
+
else:
|
846
|
+
return [x[0] for x in public_methods_with_docstrings]
|
847
|
+
|
848
|
+
|
849
|
+
class BaseDiffCollection(UserList):
|
850
|
+
"""A collection of difference objects that can be applied in sequence.
|
851
|
+
|
852
|
+
This class represents a series of differences between objects that can be
|
853
|
+
applied sequentially to transform one object into another through several steps.
|
854
|
+
"""
|
855
|
+
|
856
|
+
def __init__(self, diffs=None):
|
857
|
+
"""Initialize a new BaseDiffCollection.
|
858
|
+
|
859
|
+
Args:
|
860
|
+
diffs: Optional list of BaseDiff objects to include in the collection
|
861
|
+
"""
|
862
|
+
if diffs is None:
|
863
|
+
diffs = []
|
864
|
+
super().__init__(diffs)
|
865
|
+
|
866
|
+
def apply(self, obj: Any):
|
867
|
+
"""Apply all diffs in the collection to an object in sequence.
|
868
|
+
|
869
|
+
Args:
|
870
|
+
obj: The object to transform
|
871
|
+
|
872
|
+
Returns:
|
873
|
+
The transformed object after applying all diffs
|
874
|
+
"""
|
875
|
+
for diff in self:
|
876
|
+
obj = diff.apply(obj)
|
877
|
+
return obj
|
878
|
+
|
879
|
+
def add_diff(self, diff) -> "BaseDiffCollection":
|
880
|
+
"""Add a new diff to the collection.
|
881
|
+
|
882
|
+
Args:
|
883
|
+
diff: The BaseDiff object to add
|
884
|
+
|
885
|
+
Returns:
|
886
|
+
BaseDiffCollection: self, for method chaining
|
887
|
+
"""
|
888
|
+
self.append(diff)
|
889
|
+
return self
|
890
|
+
|
891
|
+
|
892
|
+
class DummyObject:
|
893
|
+
"""A simple class that can be used to wrap a dictionary for diffing purposes.
|
894
|
+
|
895
|
+
This utility class is used internally to compare dictionaries by adapting them
|
896
|
+
to the same interface as EDSL objects.
|
897
|
+
"""
|
898
|
+
|
899
|
+
def __init__(self, object_dict):
|
900
|
+
"""Initialize a new DummyObject.
|
901
|
+
|
902
|
+
Args:
|
903
|
+
object_dict: A dictionary to wrap
|
904
|
+
"""
|
905
|
+
self.object_dict = object_dict
|
906
|
+
|
907
|
+
def to_dict(self):
|
908
|
+
"""Get the wrapped dictionary.
|
909
|
+
|
910
|
+
Returns:
|
911
|
+
dict: The wrapped dictionary
|
912
|
+
"""
|
913
|
+
return self.object_dict
|
914
|
+
|
915
|
+
|
916
|
+
class BaseDiff:
|
917
|
+
"""Represents the differences between two EDSL objects.
|
918
|
+
|
919
|
+
This class computes and stores the differences between two objects in terms of:
|
920
|
+
- Added keys/values (present in obj2 but not in obj1)
|
921
|
+
- Removed keys/values (present in obj1 but not in obj2)
|
922
|
+
- Modified keys/values (present in both but with different values)
|
923
|
+
|
924
|
+
The differences can be displayed for inspection or applied to transform objects.
|
925
|
+
"""
|
926
|
+
|
927
|
+
def __init__(
|
928
|
+
self, obj1: Any, obj2: Any, added=None, removed=None, modified=None, level=0
|
929
|
+
):
|
930
|
+
"""Initialize a new BaseDiff between two objects.
|
931
|
+
|
932
|
+
Args:
|
933
|
+
obj1: The first object (considered the "from" object)
|
934
|
+
obj2: The second object (considered the "to" object)
|
935
|
+
added: Optional pre-computed dict of added keys/values
|
936
|
+
removed: Optional pre-computed dict of removed keys/values
|
937
|
+
modified: Optional pre-computed dict of modified keys/values
|
938
|
+
level: Nesting level for diff display formatting
|
939
|
+
"""
|
940
|
+
self.level = level
|
941
|
+
|
942
|
+
self.obj1 = obj1
|
943
|
+
self.obj2 = obj2
|
944
|
+
|
945
|
+
if "sort" in inspect.signature(obj1.to_dict).parameters:
|
946
|
+
self._dict1 = obj1.to_dict(sort=True)
|
947
|
+
self._dict2 = obj2.to_dict(sort=True)
|
948
|
+
else:
|
949
|
+
self._dict1 = obj1.to_dict()
|
950
|
+
self._dict2 = obj2.to_dict()
|
951
|
+
self._obj_class = type(obj1)
|
952
|
+
|
953
|
+
self.added = added
|
954
|
+
self.removed = removed
|
955
|
+
self.modified = modified
|
956
|
+
|
957
|
+
def __bool__(self):
|
958
|
+
"""Determine if there are any differences between the objects.
|
959
|
+
|
960
|
+
Returns:
|
961
|
+
bool: True if there are differences, False if objects are identical
|
962
|
+
"""
|
963
|
+
return bool(self.added or self.removed or self.modified)
|
964
|
+
|
965
|
+
@property
|
966
|
+
def added(self):
|
967
|
+
"""Get keys and values present in obj2 but not in obj1.
|
968
|
+
|
969
|
+
Returns:
|
970
|
+
dict: Keys and values that were added
|
971
|
+
"""
|
972
|
+
if self._added is None:
|
973
|
+
self._added = self._find_added()
|
974
|
+
return self._added
|
975
|
+
|
976
|
+
def __add__(self, other):
|
977
|
+
"""Apply this diff to another object.
|
978
|
+
|
979
|
+
This overloads the + operator to allow applying diffs with a natural syntax.
|
980
|
+
|
981
|
+
Args:
|
982
|
+
other: The object to apply the diff to
|
983
|
+
|
984
|
+
Returns:
|
985
|
+
The transformed object
|
986
|
+
"""
|
987
|
+
return self.apply(other)
|
988
|
+
|
989
|
+
@added.setter
|
990
|
+
def added(self, value):
|
991
|
+
"""Set the added keys/values.
|
992
|
+
|
993
|
+
Args:
|
994
|
+
value: Dict of added keys/values or None to compute automatically
|
995
|
+
"""
|
996
|
+
self._added = value if value is not None else self._find_added()
|
997
|
+
|
998
|
+
@property
|
999
|
+
def removed(self):
|
1000
|
+
"""Get keys and values present in obj1 but not in obj2.
|
1001
|
+
|
1002
|
+
Returns:
|
1003
|
+
dict: Keys and values that were removed
|
1004
|
+
"""
|
1005
|
+
if self._removed is None:
|
1006
|
+
self._removed = self._find_removed()
|
1007
|
+
return self._removed
|
1008
|
+
|
1009
|
+
@removed.setter
|
1010
|
+
def removed(self, value):
|
1011
|
+
"""Set the removed keys/values.
|
1012
|
+
|
1013
|
+
Args:
|
1014
|
+
value: Dict of removed keys/values or None to compute automatically
|
1015
|
+
"""
|
1016
|
+
self._removed = value if value is not None else self._find_removed()
|
1017
|
+
|
1018
|
+
@property
|
1019
|
+
def modified(self):
|
1020
|
+
"""Get keys present in both objects but with different values.
|
1021
|
+
|
1022
|
+
Returns:
|
1023
|
+
dict: Keys and their old/new values that were modified
|
1024
|
+
"""
|
1025
|
+
if self._modified is None:
|
1026
|
+
self._modified = self._find_modified()
|
1027
|
+
return self._modified
|
1028
|
+
|
1029
|
+
@modified.setter
|
1030
|
+
def modified(self, value):
|
1031
|
+
"""Set the modified keys/values.
|
1032
|
+
|
1033
|
+
Args:
|
1034
|
+
value: Dict of modified keys/values or None to compute automatically
|
1035
|
+
"""
|
1036
|
+
self._modified = value if value is not None else self._find_modified()
|
1037
|
+
|
1038
|
+
def _find_added(self) -> Dict[Any, Any]:
|
1039
|
+
"""Find keys that exist in obj2 but not in obj1.
|
1040
|
+
|
1041
|
+
Returns:
|
1042
|
+
dict: Keys and values that were added
|
1043
|
+
"""
|
1044
|
+
return {k: self._dict2[k] for k in self._dict2 if k not in self._dict1}
|
1045
|
+
|
1046
|
+
def _find_removed(self) -> Dict[Any, Any]:
|
1047
|
+
"""Find keys that exist in obj1 but not in obj2.
|
1048
|
+
|
1049
|
+
Returns:
|
1050
|
+
dict: Keys and values that were removed
|
1051
|
+
"""
|
1052
|
+
return {k: self._dict1[k] for k in self._dict1 if k not in self._dict2}
|
1053
|
+
|
1054
|
+
def _find_modified(self) -> Dict[Any, Tuple[Any, Any, str]]:
|
1055
|
+
"""Find keys that exist in both objects but have different values.
|
1056
|
+
|
1057
|
+
The difference calculation is type-aware and handles strings, dictionaries,
|
1058
|
+
and lists specially to provide more detailed difference information.
|
1059
|
+
|
1060
|
+
Returns:
|
1061
|
+
dict: Keys mapped to tuples of (old_value, new_value, diff_details)
|
1062
|
+
"""
|
1063
|
+
modified = {}
|
1064
|
+
for k in self._dict1:
|
1065
|
+
if k in self._dict2 and self._dict1[k] != self._dict2[k]:
|
1066
|
+
if isinstance(self._dict1[k], str) and isinstance(self._dict2[k], str):
|
1067
|
+
diff = self._diff_strings(self._dict1[k], self._dict2[k])
|
1068
|
+
modified[k] = (self._dict1[k], self._dict2[k], diff)
|
1069
|
+
elif isinstance(self._dict1[k], dict) and isinstance(
|
1070
|
+
self._dict2[k], dict
|
1071
|
+
):
|
1072
|
+
diff = self._diff_dicts(self._dict1[k], self._dict2[k])
|
1073
|
+
modified[k] = (self._dict1[k], self._dict2[k], diff)
|
1074
|
+
elif isinstance(self._dict1[k], list) and isinstance(
|
1075
|
+
self._dict2[k], list
|
1076
|
+
):
|
1077
|
+
d1 = dict(zip(range(len(self._dict1[k])), self._dict1[k]))
|
1078
|
+
d2 = dict(zip(range(len(self._dict2[k])), self._dict2[k]))
|
1079
|
+
diff = BaseDiff(
|
1080
|
+
DummyObject(d1), DummyObject(d2), level=self.level + 1
|
1081
|
+
)
|
1082
|
+
modified[k] = (self._dict1[k], self._dict2[k], diff)
|
1083
|
+
else:
|
1084
|
+
modified[k] = (self._dict1[k], self._dict2[k], "")
|
1085
|
+
return modified
|
1086
|
+
|
1087
|
+
@staticmethod
|
1088
|
+
def is_json(string_that_could_be_json: str) -> bool:
|
1089
|
+
"""Check if a string is valid JSON.
|
1090
|
+
|
1091
|
+
Args:
|
1092
|
+
string_that_could_be_json: The string to check
|
1093
|
+
|
1094
|
+
Returns:
|
1095
|
+
bool: True if the string is valid JSON, False otherwise
|
1096
|
+
"""
|
1097
|
+
try:
|
1098
|
+
json.loads(string_that_could_be_json)
|
1099
|
+
return True
|
1100
|
+
except json.JSONDecodeError:
|
1101
|
+
return False
|
1102
|
+
|
1103
|
+
def _diff_dicts(self, dict1: Dict[str, Any], dict2: Dict[str, Any]) -> "BaseDiff":
|
1104
|
+
"""Calculate the differences between two dictionaries.
|
1105
|
+
|
1106
|
+
Args:
|
1107
|
+
dict1: The first dictionary
|
1108
|
+
dict2: The second dictionary
|
1109
|
+
|
1110
|
+
Returns:
|
1111
|
+
BaseDiff: A difference object between the dictionaries
|
1112
|
+
"""
|
1113
|
+
diff = BaseDiff(DummyObject(dict1), DummyObject(dict2), level=self.level + 1)
|
1114
|
+
return diff
|
1115
|
+
|
1116
|
+
def _diff_strings(self, str1: str, str2: str) -> str:
|
1117
|
+
"""Calculate the differences between two strings.
|
1118
|
+
|
1119
|
+
If both strings are valid JSON, they are compared as dictionaries.
|
1120
|
+
Otherwise, they are compared line by line.
|
1121
|
+
|
1122
|
+
Args:
|
1123
|
+
str1: The first string
|
1124
|
+
str2: The second string
|
1125
|
+
|
1126
|
+
Returns:
|
1127
|
+
Union[BaseDiff, Iterable[str]]: A diff object or line-by-line differences
|
1128
|
+
"""
|
1129
|
+
if self.is_json(str1) and self.is_json(str2):
|
1130
|
+
diff = self._diff_dicts(json.loads(str1), json.loads(str2))
|
1131
|
+
return diff
|
1132
|
+
diff = difflib.ndiff(str1.splitlines(), str2.splitlines())
|
1133
|
+
return diff
|
1134
|
+
|
1135
|
+
def apply(self, obj: Any):
|
1136
|
+
"""Apply this diff to transform an object.
|
1137
|
+
|
1138
|
+
This method applies the computed differences to an object, adding new keys,
|
1139
|
+
removing deleted keys, and updating modified values.
|
1140
|
+
|
1141
|
+
Args:
|
1142
|
+
obj: The object to transform
|
1143
|
+
|
1144
|
+
Returns:
|
1145
|
+
The transformed object
|
1146
|
+
"""
|
1147
|
+
new_obj_dict = obj.to_dict()
|
1148
|
+
for k, v in self.added.items():
|
1149
|
+
new_obj_dict[k] = v
|
1150
|
+
for k in self.removed.keys():
|
1151
|
+
del new_obj_dict[k]
|
1152
|
+
for k, (v1, v2, diff) in self.modified.items():
|
1153
|
+
new_obj_dict[k] = v2
|
1154
|
+
|
1155
|
+
return obj.from_dict(new_obj_dict)
|
1156
|
+
|
1157
|
+
def to_dict(self) -> Dict[str, Any]:
|
1158
|
+
"""Serialize this difference object to a dictionary.
|
1159
|
+
|
1160
|
+
Returns:
|
1161
|
+
dict: A dictionary representation of the differences
|
1162
|
+
"""
|
1163
|
+
return {
|
1164
|
+
"added": self.added,
|
1165
|
+
"removed": self.removed,
|
1166
|
+
"modified": self.modified,
|
1167
|
+
"obj1": self._dict1,
|
1168
|
+
"obj2": self._dict2,
|
1169
|
+
"obj_class": self._obj_class.__name__,
|
1170
|
+
"level": self.level,
|
1171
|
+
}
|
1172
|
+
|
1173
|
+
@classmethod
|
1174
|
+
def from_dict(cls, diff_dict: Dict[str, Any], obj1: Any, obj2: Any):
|
1175
|
+
"""Create a BaseDiff from a dictionary representation.
|
1176
|
+
|
1177
|
+
Args:
|
1178
|
+
diff_dict: Dictionary containing the difference data
|
1179
|
+
obj1: The first object
|
1180
|
+
obj2: The second object
|
1181
|
+
|
1182
|
+
Returns:
|
1183
|
+
BaseDiff: A new difference object
|
1184
|
+
"""
|
1185
|
+
return cls(
|
1186
|
+
obj1=obj1,
|
1187
|
+
obj2=obj2,
|
1188
|
+
added=diff_dict["added"],
|
1189
|
+
removed=diff_dict["removed"],
|
1190
|
+
modified=diff_dict["modified"],
|
1191
|
+
level=diff_dict["level"],
|
1192
|
+
)
|
1193
|
+
|
1194
|
+
class Results(UserList):
|
1195
|
+
"""Helper class for storing and formatting difference results.
|
1196
|
+
|
1197
|
+
This class extends UserList to provide indentation and formatting
|
1198
|
+
capabilities when displaying differences.
|
1199
|
+
"""
|
1200
|
+
|
1201
|
+
def __init__(self, prepend=" ", level=0):
|
1202
|
+
"""Initialize a new Results collection.
|
1203
|
+
|
1204
|
+
Args:
|
1205
|
+
prepend: The string to use for indentation
|
1206
|
+
level: The nesting level
|
1207
|
+
"""
|
1208
|
+
super().__init__()
|
1209
|
+
self.prepend = prepend
|
1210
|
+
self.level = level
|
1211
|
+
|
1212
|
+
def append(self, item):
|
1213
|
+
"""Add an item to the results with proper indentation.
|
1214
|
+
|
1215
|
+
Args:
|
1216
|
+
item: The string to add
|
1217
|
+
"""
|
1218
|
+
super().append(self.prepend * self.level + item)
|
1219
|
+
|
1220
|
+
def __str__(self):
|
1221
|
+
"""Generate a human-readable string representation of the differences.
|
1222
|
+
|
1223
|
+
Returns:
|
1224
|
+
str: A formatted string showing the differences
|
1225
|
+
"""
|
1226
|
+
prepend = " "
|
1227
|
+
result = self.Results(level=self.level, prepend="\t")
|
1228
|
+
if self.added:
|
1229
|
+
result.append("Added keys and values:")
|
1230
|
+
for k, v in self.added.items():
|
1231
|
+
result.append(prepend + f" {k}: {v}")
|
1232
|
+
if self.removed:
|
1233
|
+
result.append("Removed keys and values:")
|
1234
|
+
for k, v in self.removed.items():
|
1235
|
+
result.append(f" {k}: {v}")
|
1236
|
+
if self.modified:
|
1237
|
+
result.append("Modified keys and values:")
|
1238
|
+
for k, (v1, v2, diff) in self.modified.items():
|
1239
|
+
result.append(f"Key: {k}:")
|
1240
|
+
result.append(f" Old value: {v1}")
|
1241
|
+
result.append(f" New value: {v2}")
|
1242
|
+
if diff:
|
1243
|
+
result.append(f" Diff:")
|
1244
|
+
try:
|
1245
|
+
for line in diff:
|
1246
|
+
result.append(f" {line}")
|
1247
|
+
except:
|
1248
|
+
result.append(f" {diff}")
|
1249
|
+
return "\n".join(result)
|
1250
|
+
|
1251
|
+
def __repr__(self):
|
1252
|
+
"""Generate a developer-friendly string representation.
|
1253
|
+
|
1254
|
+
Returns:
|
1255
|
+
str: A representation that can be used to recreate the object
|
1256
|
+
"""
|
1257
|
+
return (
|
1258
|
+
f"BaseDiff(obj1={self.obj1!r}, obj2={self.obj2!r}, added={self.added!r}, "
|
1259
|
+
f"removed={self.removed!r}, modified={self.modified!r})"
|
1260
|
+
)
|
1261
|
+
|
1262
|
+
def add_diff(self, diff) -> "BaseDiffCollection":
|
1263
|
+
"""Combine this diff with another into a collection.
|
1264
|
+
|
1265
|
+
Args:
|
1266
|
+
diff: Another BaseDiff object
|
1267
|
+
|
1268
|
+
Returns:
|
1269
|
+
BaseDiffCollection: A collection containing both diffs
|
1270
|
+
"""
|
1271
|
+
from edsl.base import BaseDiffCollection
|
1272
|
+
return BaseDiffCollection([self, diff])
|
1273
|
+
|
1274
|
+
|
1275
|
+
if __name__ == "__main__":
|
1276
|
+
import doctest
|
1277
|
+
doctest.testmod()
|
1278
|
+
|
1279
|
+
from edsl import Question
|
1280
|
+
|
1281
|
+
q_ft = Question.example("free_text")
|
1282
|
+
q_mc = Question.example("multiple_choice")
|
1283
|
+
|
1284
|
+
diff1 = q_ft - q_mc
|
1285
|
+
assert q_ft == q_mc + diff1
|
1286
|
+
assert q_ft == diff1.apply(q_mc)
|
1287
|
+
|
1288
|
+
# ## Test chain of diffs
|
1289
|
+
q0 = Question.example("free_text")
|
1290
|
+
q1 = q0.copy()
|
1291
|
+
q1.question_text = "Why is Buzzard's Bay so named?"
|
1292
|
+
diff1 = q1 - q0
|
1293
|
+
q2 = q1.copy()
|
1294
|
+
q2.question_name = "buzzard_bay"
|
1295
|
+
diff2 = q2 - q1
|
1296
|
+
|
1297
|
+
diff_chain = diff1.add_diff(diff2)
|
1298
|
+
|
1299
|
+
new_q2 = diff_chain.apply(q0)
|
1300
|
+
assert new_q2 == q2
|
1301
|
+
|
1302
|
+
new_q2 = diff_chain + q0
|
1303
|
+
assert new_q2 == q2
|