codeplain 0.3.8__py3-none-any.whl → 0.3.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeplain
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -4,13 +4,14 @@ concept_utils.py,sha256=D4qVHFBRvfJZwiIq1AD_SV0Ev-RnOMg2yGWo6Cal7PM,7913
4
4
  diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
5
5
  event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
6
6
  file_utils.py,sha256=MNZ5li1ofIMOYB6FCrMa_UkX6nkGCgSZuaqzhqVc1Ok,11682
7
- git_utils.py,sha256=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
8
- memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
9
- module_renderer.py,sha256=DZCIo_QzvhFw8EqQTKeF3HUbvfumrV2O1sz-gUhWpIA,6752
7
+ git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
8
+ memory_management.py,sha256=SPoK0BXB9_yDyrrOKjFtOOlKkJIq0I5EXV7dE0OsE8g,5972
9
+ metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
10
+ module_renderer.py,sha256=AUbjGiE6USMe-uTfbOs9gEQSEMzO9qLwGTxy3_tC_M4,6615
10
11
  partial_rendering.py,sha256=0ChOaGytogBUEGISpbWkbjT9Hqy8pnOkJdJXcitJDgk,12219
11
12
  path_resolution.py,sha256=0tMqU0a1g4DYhTOp7QjLVSEkv_cgasb4NI1mK809dUM,2233
12
- plain2code.py,sha256=YLwwzAvE0EWz51V9p8b_kbTltkPvhA2Q8J4JrYyMrAM,15327
13
- plain2code_arguments.py,sha256=3tAA9Haw-j97zztjXdhi6LxFtURP0Tfg9J_rvJDoXmw,17685
13
+ plain2code.py,sha256=Hai5_Ho1nY0Hau7Y6QEGkYeg8NKVx5jDMtQV0UVHsLU,15706
14
+ plain2code_arguments.py,sha256=knY7gqfpj2IKnPSl-dEdxBUpzrbohEQXamNYkFVC00Y,17426
14
15
  plain2code_console.py,sha256=Ifs8tmfNX31UujQ5_0sk9AJcajMG_6qhF-6QZcfbcEE,5773
15
16
  plain2code_events.py,sha256=rgDF5PDtNr_lEPvJmd1tIQXZrzwgzZW5b-_pykugqe4,1587
16
17
  plain2code_exceptions.py,sha256=wkkEXw2k4LCd09PIH3_Hu0HyZvtXYjFaA653Sfey4FA,2287
@@ -18,12 +19,12 @@ plain2code_logger.py,sha256=NRPLVIMnCHNVl6dII9gFoIzMdr8xa7icnL0T_Fx2XLw,4839
18
19
  plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
19
20
  plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
20
21
  plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
21
- plain2code_telemetry.py,sha256=7yuTH7mQE_wGVW2H1oiVZ3UJg98ArB5HcUUm9isnv9g,4354
22
+ plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
22
23
  plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
23
24
  plain_file.py,sha256=k2rcq53q0kOf9_8egrZILpkrVkZdfJt7HU0gHBFskU4,35116
24
- plain_modules.py,sha256=uvRPfxBJwstaWi5lgYKgtQdhhf0Naiy2mS41tLw_TjY,16186
25
+ plain_modules.py,sha256=VgOwEWVhp7dyRs5NM-jVaC5GKn8KzT1sWuSNkM7YLIA,17375
25
26
  plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
26
- system_config.py,sha256=VyE6MhXfi6agZvE02p_G4K8yCHkLh3eu890cBzPyDvs,2196
27
+ system_config.py,sha256=nyeDS1WkyA-5m3TflB_XMDyhBvC-tVRAG3VgJaZYG_M,3694
27
28
  usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
28
29
  cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
29
30
  cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
@@ -35,11 +36,11 @@ docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
35
36
  examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
36
37
  render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt1M,3959
38
- render_machine/conformance_tests.py,sha256=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
39
+ render_machine/conformance_tests.py,sha256=jvdWRx-ZURSq4JMase1svKzYJ98Lc4svPvfHXglHeFE,7163
39
40
  render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
40
- render_machine/render_context.py,sha256=eoaO807LntIRYjLSpc3x7bvwBnFfzLlHmKkyKlIRrxs,25697
41
+ render_machine/render_context.py,sha256=pdosCY7QMzfPy-cJxEsiM9m4I5K2CoNJ7N-5Fzg0NdU,25584
41
42
  render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
42
- render_machine/render_utils.py,sha256=Jb9NJ73yV1qCfGy9S5A9zcnz0FGVt3MHG8Ofk-9A-aI,9355
43
+ render_machine/render_utils.py,sha256=yA3X15E_Lebk_-MxkSz9rM_qy-Pes8EQ9Ye-smkHCDw,9320
43
44
  render_machine/state_machine_config.py,sha256=NnX5sBGkc6cNG8OOQ4PrJeJBCADRGZKJYQXV7ndR4bs,26789
44
45
  render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
45
46
  render_machine/triggers.py,sha256=VekNhHGR-u5JXIboqc_Eyqf73LNH7wezLK7r3WWtodk,1269
@@ -52,7 +53,7 @@ render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9
52
53
  render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
53
54
  render_machine/actions/fix_conformance_test.py,sha256=EZLVjF4A8zSTmyqdgcLoz-2pLpeieq4M_Rb71pY3IKA,10095
54
55
  render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
55
- render_machine/actions/prepare_repositories.py,sha256=eE-uU6H_K5IQMraVWSRi4TKk_XPrnA878T_JwtI9cWo,3410
56
+ render_machine/actions/prepare_repositories.py,sha256=dnh4cYeTZS0snWEeD8gAxtW2ot3KBq4nStRG052qc9U,3124
56
57
  render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
57
58
  render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
58
59
  render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
@@ -66,34 +67,36 @@ standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOz
66
67
  standard_template_library/typescript-react-app-template.plain,sha256=DN5wEJcT8ZqQTf0vFKi9Z1isi9cin2UVNOZb3r28d3M,715
67
68
  tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
68
69
  tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
69
- tests/test_arg_provenance.py,sha256=Vp-ZtNbTlSNOE8iiHOin3YI9sobbN0tRPUIfrYjxttQ,3459
70
+ tests/test_arg_provenance.py,sha256=d_J7aCeHMXBpctBWTt60WU-I8pEKN7XRXRTJZZGCdo0,3441
70
71
  tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
71
72
  tests/test_cli_output.py,sha256=iKS1Li4yhoIL0ezJ58o4oEf9V8TiKcQkvQLVtJpdM4c,18420
72
73
  tests/test_console_log_styles.py,sha256=63DQWiwWNhe_rMP70bW9DAB6-EPaqU3Wpwyh8eR09pc,4080
73
74
  tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
74
- tests/test_folder_path_resolution.py,sha256=_ZvVFu5m2-AH8iQNn0fCYkA0flVxVwdh1oQiWcPNIxw,6611
75
+ tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
75
76
  tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
76
77
  tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
77
78
  tests/test_log_file_name_resolution.py,sha256=99g9NgPF-BZ98CvNyqDcltBAx4sLBE8gpQPAAieKiPc,3619
78
79
  tests/test_logging_config_path_resolution.py,sha256=avz_ehRd18INwIXvAy1AwVzNNUz0vS8eu1nRe0bra8g,2650
79
80
  tests/test_partial_rendering.py,sha256=r0RUUd1pI4-2ZkRrM1j8_kUqv-70hrOGjuKBhZoRmH0,22758
80
81
  tests/test_path_resolution.py,sha256=yQpd-Unq1lwNv0wkRPwVs5pDT8JmyyDgbpiTQsyt--Y,2615
81
- tests/test_plain2code.py,sha256=DQYFc0JVSkMI8LZCNaQ__2Nph-DcZZW17rprJA54eyw,3851
82
+ tests/test_plain2code.py,sha256=lyb7iYK8XHu1dmTFh9Q31YgX0faNlCHiyI5M35VQwOg,3780
82
83
  tests/test_plain2code_state.py,sha256=cowPZS9oRbv9J49CE4P3Rc47Q5oQOby4bTf2OlnRlBI,1217
83
84
  tests/test_plain2code_utils.py,sha256=-W8a7FquMJQ_pTdBoBF1XprzNL2G555QhdtXCy6BDyQ,1237
84
- tests/test_plain_modules.py,sha256=S7sd4ZYNotXOgCyZzmBd714QFLih69nlZ3tV8WNtVPo,13766
85
+ tests/test_plain_modules.py,sha256=jDjPv1JyKCgIMaoy3GYpjxTSQkfkI2ppmJgqzHdQ6E4,21592
85
86
  tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
86
87
  tests/test_plainfileparser.py,sha256=xd9P0KqZJlJPn3RKT510IQbfBotk6nQ9O8p9iLwJiQk,22815
87
88
  tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
89
+ tests/test_prepare_repositories_layout.py,sha256=9T9lf92HhZC_ziUZxiyogbbeqJfmg131FZS81BuUNfs,4751
88
90
  tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
89
91
  tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
90
92
  tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
91
- tests/test_telemetry.py,sha256=hjm81aRQQZcZ0RB6aVbS3WO46_nIAii_csmbbtnUh74,7110
93
+ tests/test_system_config.py,sha256=poH6eviXqAxAzTcvdP0TeS0wBp6CW1IW5irqto7FNyI,1784
94
+ tests/test_telemetry.py,sha256=LINvx1_Oz1OJ3ho2RcIcAA29IsNfUbkVVCbFaQcrBaA,8133
92
95
  tests/test_tui_usage.py,sha256=sfiAGTpRrT3MRr0-8DH3qjuECSxjODVgwzrK_K3iPXs,7959
93
96
  tests/test_usage_summary.py,sha256=Ps0kVzvmdZYeXrBR-cqkn3szTV5neYKs1PmmURngeIc,1079
94
97
  tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
95
- tests/e2e/test_hello_world_python.py,sha256=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
96
- tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
98
+ tests/e2e/test_hello_world_python.py,sha256=2Cgs3-79dtQh5gVab257tFDHXTznefZeYqbTjlM4Nng,1632
99
+ tests/e2e/test_hello_world_python_windows.py,sha256=ggynH_Wm4011M500zoPNNgvD-s4vZ3hvlorf1yARgQQ,2019
97
100
  tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
98
101
  tui/components.py,sha256=XkC4Aq6VyTnFnBQs8PEHwVdAOczhm0ebKN92kiYMbq0,25791
99
102
  tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
@@ -103,8 +106,8 @@ tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
103
106
  tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
104
107
  tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
105
108
  tui/widget_helpers.py,sha256=ToO9MV2bF2Rp-XMNcjIYclnB_qmgO5sLw4UtQ2CyTMg,6652
106
- codeplain-0.3.8.dist-info/METADATA,sha256=nfj-9PV0EN0MS1W9ILf1GyOnSfcB-YSyEXW04UY9vHQ,6351
107
- codeplain-0.3.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
108
- codeplain-0.3.8.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
109
- codeplain-0.3.8.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
110
- codeplain-0.3.8.dist-info/RECORD,,
109
+ codeplain-0.3.9.dist-info/METADATA,sha256=v3UbwneHulzIUZMJI9ZHf032CyyhAef-BBmn-RRsq14,6351
110
+ codeplain-0.3.9.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
111
+ codeplain-0.3.9.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
112
+ codeplain-0.3.9.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
113
+ codeplain-0.3.9.dist-info/RECORD,,
git_utils.py CHANGED
@@ -64,7 +64,6 @@ def init_git_repo(
64
64
  path_to_repo: Union[str, os.PathLike],
65
65
  module_name: Optional[str] = None,
66
66
  render_id: Optional[str] = None,
67
- initial_files: Optional[dict[str, str]] = None,
68
67
  ) -> Repo:
69
68
  """
70
69
  Initializes a new git repository in the given path.
@@ -79,10 +78,6 @@ def init_git_repo(
79
78
  repo = Repo.init(path_to_repo)
80
79
  _ensure_git_config(repo)
81
80
 
82
- if initial_files:
83
- file_utils.store_response_files(path_to_repo, initial_files, [])
84
- repo.git.add(".")
85
-
86
81
  repo.git.commit(
87
82
  "--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
88
83
  )
@@ -95,14 +90,9 @@ def clone_repo(
95
90
  new_repo_path: str,
96
91
  module_name: Optional[str] = None,
97
92
  render_id: Optional[str] = None,
98
- initial_files: Optional[dict[str, str]] = None,
99
93
  ) -> Repo:
100
94
  repo = Repo.clone_from(source_repo_path, new_repo_path)
101
95
 
102
- if initial_files:
103
- file_utils.store_response_files(new_repo_path, initial_files, [])
104
- repo.git.add(".")
105
-
106
96
  repo.git.commit(
107
97
  "--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
108
98
  )
memory_management.py CHANGED
@@ -3,7 +3,6 @@ import os
3
3
 
4
4
  import file_utils
5
5
  from plain2code_console import console
6
- from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER
7
6
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
8
7
  from render_machine.render_context import RenderContext
9
8
 
@@ -24,9 +23,9 @@ class MemoryManager:
24
23
  console.debug(f"Loaded {len(memory_files_content)} memory files.")
25
24
  return memory_files, memory_files_content
26
25
 
27
- def __init__(self, codeplain_api, module_name: str, conformance_tests_folder: str):
26
+ def __init__(self, codeplain_api, memory_folder: str):
28
27
  self.codeplain_api = codeplain_api
29
- self.memory_folder = os.path.join(conformance_tests_folder, module_name, CODEPLAIN_MEMORY_SUBFOLDER)
28
+ self.memory_folder = memory_folder
30
29
 
31
30
  def create_conformance_tests_memory(
32
31
  self, render_context: RenderContext, exit_code: int, conformance_tests_issue: str
metadata_utils.py ADDED
@@ -0,0 +1,46 @@
1
+ """Read/write helpers for the per-module metadata file (``module_metadata.json``).
2
+
3
+ These helpers operate purely on a metadata path or a plain dict. Module-specific
4
+ logic — computing the hashes, assembling the payload from the spec and required
5
+ modules — stays on :class:`plain_modules.PlainModule`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+
13
+ MODULE_METADATA_FILENAME = "module_metadata.json"
14
+ MODULE_FUNCTIONALITIES = "functionalities"
15
+ REQUIRED_MODULES_FUNCTIONALITIES = "required_modules_functionalities"
16
+
17
+
18
+ def load_metadata(metadata_path: str) -> dict | None:
19
+ """Return the parsed metadata dict, or None if the file does not exist."""
20
+ if not os.path.exists(metadata_path):
21
+ return None
22
+
23
+ with open(metadata_path, "r", encoding="utf-8") as f:
24
+ return json.load(f)
25
+
26
+
27
+ def write_metadata(metadata_path: str, metadata: dict) -> None:
28
+ """Write metadata as indented JSON, creating the parent folder if needed."""
29
+ os.makedirs(os.path.dirname(metadata_path), exist_ok=True)
30
+
31
+ with open(metadata_path, "w", encoding="utf-8") as f:
32
+ json.dump(metadata, f, indent=4)
33
+
34
+
35
+ def truncate_functionalities(metadata: dict, keep_count: int) -> bool:
36
+ """Trim the functionalities list in ``metadata`` in place to ``keep_count`` entries.
37
+
38
+ Returns True if the list was shortened, False if it was already short enough
39
+ (in which case ``metadata`` is left untouched).
40
+ """
41
+ functionalities = metadata.get(MODULE_FUNCTIONALITIES, [])
42
+ if len(functionalities) <= keep_count:
43
+ return False
44
+
45
+ metadata[MODULE_FUNCTIONALITIES] = functionalities[:keep_count]
46
+ return True
module_renderer.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import argparse
2
- import os
3
2
  import threading
4
3
 
5
4
  from event_bus import EventBus
@@ -48,9 +47,8 @@ class ModuleRenderer:
48
47
  self.codeplainAPI,
49
48
  memory_manager,
50
49
  plain_module,
51
- build_folder=os.path.join(self.args.build_folder, plain_module.module_name),
50
+ build_folder=plain_module.module_build_folder,
52
51
  build_dest=self.args.build_dest,
53
- conformance_tests_folder=self.args.conformance_tests_folder,
54
52
  conformance_tests_dest=self.args.conformance_tests_dest,
55
53
  unittests_script=self.args.unittests_script,
56
54
  conformance_tests_script=self.args.conformance_tests_script,
@@ -115,8 +113,7 @@ class ModuleRenderer:
115
113
 
116
114
  memory_manager = MemoryManager(
117
115
  self.codeplainAPI,
118
- plain_module.module_name,
119
- self.args.conformance_tests_folder,
116
+ plain_module.module_memory_folder,
120
117
  )
121
118
  render_context = self._build_render_context_for_module(
122
119
  plain_module,
@@ -165,7 +162,7 @@ class ModuleRenderer:
165
162
  if self.args.copy_build:
166
163
  rendered_code_path = f"{self.args.build_dest}/"
167
164
  else:
168
- rendered_code_path = self.args.build_folder
165
+ rendered_code_path = self.plain_module.module_build_folder
169
166
 
170
167
  self.run_state.set_render_generated_code_path(rendered_code_path)
171
168
  self.event_bus.publish(RenderCompleted(rendered_code_path=rendered_code_path))
plain2code.py CHANGED
@@ -212,6 +212,12 @@ def render( # noqa: C901
212
212
 
213
213
  warn_if_acceptance_tests_without_conformance_script(plain_module, args)
214
214
 
215
+ # The module_metadata file lives outside the code git repo. That means that a crash mid-render can leave it
216
+ # claiming a functionality was implemented even thought it wasn't yet committed (because of the crash).
217
+ # Out of precaution, this reconciles every module_metadata against the code repo.
218
+ for module in plain_module.all_required_modules + [plain_module]:
219
+ module.reconcile_metadata_with_git()
220
+
215
221
  render_choice = None
216
222
  if render_range is None:
217
223
  plain_module_render_state = get_plain_module_render_state(plain_module)
@@ -356,7 +362,6 @@ def main(): # noqa: C901
356
362
  plain_module = plain_modules.PlainModule(
357
363
  os.path.basename(args.filename),
358
364
  args.build_folder,
359
- args.conformance_tests_folder,
360
365
  template_dirs,
361
366
  )
362
367
  except Exception as e:
plain2code_arguments.py CHANGED
@@ -16,7 +16,6 @@ CODEPLAIN_API_KEY = os.getenv("CODEPLAIN_API_KEY")
16
16
 
17
17
 
18
18
  DEFAULT_BUILD_FOLDER = "plain_modules"
19
- DEFAULT_CONFORMANCE_TESTS_FOLDER = "conformance_tests"
20
19
  DEFAULT_BUILD_DEST = "dist"
21
20
  DEFAULT_CONFORMANCE_TESTS_DEST = "dist_conformance_tests"
22
21
 
@@ -299,23 +298,15 @@ def create_parser():
299
298
  parser,
300
299
  "--unittests-script",
301
300
  type=str,
302
- help="Shell script to run unit tests on generated code. Receives the build folder path as its first argument (default: 'plain_modules').",
303
- )
304
- _add_arg(
305
- parser,
306
- "--conformance-tests-folder",
307
- type=non_empty_string,
308
- default=DEFAULT_CONFORMANCE_TESTS_FOLDER,
309
- help="Folder for conformance test files",
310
- path=True,
301
+ help="Shell script to run unit tests on generated code. Receives the module's code folder path as its first argument (e.g. `plain_modules/module_name/code`).",
311
302
  )
312
303
  _add_arg(
313
304
  parser,
314
305
  "--conformance-tests-script",
315
306
  type=str,
316
307
  help="Path to conformance tests shell script. Every conformance test script should accept two arguments: "
317
- "1) Path to a folder (e.g. `plain_modules/module_name`) containing generated source code, "
318
- "2) Path to a subfolder of the conformance tests folder (e.g. `conformance_tests/subfoldername`) containing test files.",
308
+ "1) Path to a folder (e.g. `plain_modules/module_name/code`) containing generated source code, "
309
+ "2) Path to a subfolder of the module's tests folder (e.g. `plain_modules/module_name/tests/subfoldername`) containing test files.",
319
310
  )
320
311
 
321
312
  _add_arg(
@@ -400,7 +391,7 @@ def create_parser():
400
391
  "--copy-conformance-tests",
401
392
  action="store_true",
402
393
  default=False,
403
- help="If set, copy the conformance tests of code in `--conformance-tests-folder` folder to `--conformance-tests-dest` folder successful rendering. Requires --conformance-tests-script.",
394
+ help="If set, copy the module's conformance tests (from `<build-folder>/<module>/tests`) to `--conformance-tests-dest` folder after successful rendering. Requires --conformance-tests-script.",
404
395
  )
405
396
  _add_arg(
406
397
  parser,
@@ -490,8 +481,8 @@ def parse_arguments(command_line: Optional[Sequence[str]] = None):
490
481
 
491
482
  if args.build_folder == args.build_dest:
492
483
  parser.error("--build-folder and --build-dest cannot be the same")
493
- if args.conformance_tests_folder == args.conformance_tests_dest:
494
- parser.error("--conformance-tests-folder and --conformance-tests-dest cannot be the same")
484
+ if args.conformance_tests_dest == args.build_folder:
485
+ parser.error("--conformance-tests-dest and --build-folder cannot be the same")
495
486
 
496
487
  args.render_conformance_tests = args.conformance_tests_script is not None
497
488
 
plain2code_telemetry.py CHANGED
@@ -1,9 +1,10 @@
1
1
  """Crash reporting via Sentry.
2
2
 
3
3
  Only unexpected exceptions are reported (the caller decides which exceptions are
4
- expected; see EXPECTED_EXCEPTIONS in plain2code.py). Reporting is on by default
5
- and can be disabled by setting the CODEPLAIN_NO_TELEMETRY environment variable
6
- to any non-empty value.
4
+ expected; see EXPECTED_EXCEPTIONS in plain2code.py). In production, reporting is
5
+ on by default and can be disabled by setting CODEPLAIN_TELEMETRY to 0, false or
6
+ off. In any other environment it is off unless CODEPLAIN_TELEMETRY is explicitly
7
+ set to 1, true or on.
7
8
  """
8
9
 
9
10
  import os
@@ -20,9 +21,7 @@ from system_config import system_config
20
21
 
21
22
  SENTRY_DSN = "https://64d0d86b50b34e2dede3e4eaf5142282@o4510793955934208.ingest.us.sentry.io/4511540621213696"
22
23
 
23
- NO_TELEMETRY_ENV_VAR = "CODEPLAIN_NO_TELEMETRY"
24
- ENVIRONMENT_ENV_VAR = "CODEPLAIN_ENV"
25
- DEFAULT_ENVIRONMENT = "production"
24
+ TELEMETRY_ENV_VAR = "CODEPLAIN_TELEMETRY"
26
25
 
27
26
  FLUSH_TIMEOUT_SECONDS = 2
28
27
 
@@ -50,10 +49,17 @@ SCRUB_DENYLIST = DEFAULT_DENYLIST + [
50
49
 
51
50
 
52
51
  def telemetry_enabled() -> bool:
53
- """Return True if crash reporting should be active."""
54
- if os.environ.get(NO_TELEMETRY_ENV_VAR):
55
- return False
56
- return True
52
+ """Return True if crash reporting should be active.
53
+
54
+ In production it is on unless CODEPLAIN_TELEMETRY disables it.
55
+ Anywhere else it is off unless CODEPLAIN_TELEMETRY enables it.
56
+ """
57
+ setting = os.environ.get(TELEMETRY_ENV_VAR, "").strip().lower()
58
+
59
+ if system_config.environment == "production":
60
+ return setting not in {"0", "false", "off"}
61
+
62
+ return setting in {"1", "true", "on"}
57
63
 
58
64
 
59
65
  def initialize_telemetry(**init_overrides: Any) -> bool:
@@ -65,7 +71,7 @@ def initialize_telemetry(**init_overrides: Any) -> bool:
65
71
  init_kwargs: dict[str, Any] = dict(
66
72
  dsn=SENTRY_DSN,
67
73
  release=system_config.client_version,
68
- environment=os.environ.get(ENVIRONMENT_ENV_VAR, DEFAULT_ENVIRONMENT),
74
+ environment=system_config.environment,
69
75
  send_default_pii=False,
70
76
  server_name="", # hostname is identifying; don't send it
71
77
  default_integrations=False,
plain_modules.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  import os
5
4
  import shutil
6
5
  from functools import cached_property
@@ -13,16 +12,29 @@ except ImportError:
13
12
  raise GitNotInstalledError("git is not installed. Please install git and try again.")
14
13
 
15
14
  import git_utils
15
+ import metadata_utils
16
16
  import plain_file
17
17
  import plain_spec
18
+ from metadata_utils import (
19
+ MODULE_FUNCTIONALITIES,
20
+ MODULE_METADATA_FILENAME,
21
+ REQUIRED_MODULES_FUNCTIONALITIES,
22
+ )
18
23
  from plain2code_console import console
19
24
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
20
25
 
21
26
  CODEPLAIN_MEMORY_SUBFOLDER = ".memory"
22
27
  CODEPLAIN_METADATA_FOLDER = ".codeplain"
23
- MODULE_METADATA_FILENAME = "module_metadata.json"
24
- MODULE_FUNCTIONALITIES = "functionalities"
25
- REQUIRED_MODULES_FUNCTIONALITIES = "required_modules_functionalities"
28
+ MODULE_CODE_SUBFOLDER = "code"
29
+ MODULE_TESTS_SUBFOLDER = "tests"
30
+
31
+
32
+ def get_module_code_folder(modules_base_folder: str, module_name: str) -> str:
33
+ return os.path.join(modules_base_folder, module_name, MODULE_CODE_SUBFOLDER)
34
+
35
+
36
+ def get_module_tests_folder(modules_base_folder: str, module_name: str) -> str:
37
+ return os.path.join(modules_base_folder, module_name, MODULE_TESTS_SUBFOLDER)
26
38
 
27
39
 
28
40
  def _strip_functional_requirements(plain_source_tree: dict) -> dict:
@@ -33,10 +45,9 @@ def _strip_functional_requirements(plain_source_tree: dict) -> dict:
33
45
 
34
46
 
35
47
  class PlainModule:
36
- def __init__(self, filename: str, build_folder: str, conformance_tests_folder: str, template_dirs: list[str]):
48
+ def __init__(self, filename: str, build_folder: str, template_dirs: list[str]):
37
49
  self.filename = filename
38
50
  self.build_folder = build_folder
39
- self.conformance_tests_folder = conformance_tests_folder
40
51
  self.template_dirs = template_dirs
41
52
  module_name, plain_source, required_modules_names = plain_file.plain_file_parser(
42
53
  self.filename, self.template_dirs
@@ -53,7 +64,6 @@ class PlainModule:
53
64
  PlainModule(
54
65
  plain_file.get_filename_from_module_name(module_name),
55
66
  self.build_folder,
56
- self.conformance_tests_folder,
57
67
  self.template_dirs,
58
68
  )
59
69
  for module_name in required_modules_names
@@ -70,16 +80,24 @@ class PlainModule:
70
80
 
71
81
  return all_required_modules
72
82
 
83
+ @property
84
+ def module_folder(self):
85
+ return os.path.join(self.build_folder, self.module_name)
86
+
73
87
  @property
74
88
  def module_conformance_tests_folder(self):
75
- return os.path.join(self.conformance_tests_folder, self.module_name)
89
+ return get_module_tests_folder(self.build_folder, self.module_name)
76
90
 
77
91
  @property
78
92
  def module_build_folder(self):
79
- return os.path.join(self.build_folder, self.module_name)
93
+ return get_module_code_folder(self.build_folder, self.module_name)
94
+
95
+ @property
96
+ def module_memory_folder(self):
97
+ return os.path.join(self.module_folder, CODEPLAIN_MEMORY_SUBFOLDER)
80
98
 
81
99
  def get_codeplain_folder(self):
82
- return os.path.join(self.module_build_folder, CODEPLAIN_METADATA_FOLDER)
100
+ return os.path.join(self.module_folder, CODEPLAIN_METADATA_FOLDER)
83
101
 
84
102
  def get_module_render_status(self) -> tuple[str | None, str | None]:
85
103
  module_name, frid = git_utils.get_last_rendered_functionality(self.module_build_folder)
@@ -104,16 +122,7 @@ class PlainModule:
104
122
  return repo
105
123
 
106
124
  def load_module_metadata(self) -> dict | None:
107
- codeplain_folder = self.get_codeplain_folder()
108
- if not os.path.exists(codeplain_folder):
109
- return None
110
-
111
- metadata_path = os.path.join(codeplain_folder, MODULE_METADATA_FILENAME)
112
- if not os.path.exists(metadata_path):
113
- return None
114
-
115
- with open(metadata_path, "r", encoding="utf-8") as f:
116
- return json.load(f)
125
+ return metadata_utils.load_metadata(self.module_metadata_path())
117
126
 
118
127
  def update_frid_in_module_metadata(self, frid: str) -> None:
119
128
  # Store the raw FR markdown (with any {{ code_variable }} placeholders intact), exactly
@@ -130,10 +139,7 @@ class PlainModule:
130
139
  functionalities.append(frid_text)
131
140
  metadata[MODULE_FUNCTIONALITIES] = functionalities
132
141
 
133
- codeplain_folder = self.get_codeplain_folder()
134
- os.makedirs(codeplain_folder, exist_ok=True)
135
- with open(self.module_metadata_path(), "w", encoding="utf-8") as f:
136
- json.dump(metadata, f, indent=4)
142
+ metadata_utils.write_metadata(self.module_metadata_path(), metadata)
137
143
 
138
144
  def get_module_source_hash(self) -> str:
139
145
  return plain_spec.get_hash_value([self.plain_source] + self.resources_list)
@@ -186,10 +192,7 @@ class PlainModule:
186
192
 
187
193
  return functionalities
188
194
 
189
- def module_metadata_path(self, for_git_repo: bool = False) -> str:
190
- if for_git_repo:
191
- return os.path.join(CODEPLAIN_METADATA_FOLDER, MODULE_METADATA_FILENAME)
192
-
195
+ def module_metadata_path(self) -> str:
193
196
  return os.path.join(self.get_codeplain_folder(), MODULE_METADATA_FILENAME)
194
197
 
195
198
  def get_hashes(self) -> dict[str, str]:
@@ -201,12 +204,46 @@ class PlainModule:
201
204
  hashes["required_modules_code_hash"] = self.required_modules[-1].get_module_code_hash()
202
205
  return hashes
203
206
 
204
- def save_module_metadata(self):
205
- codeplain_folder = self.get_codeplain_folder()
206
- os.makedirs(codeplain_folder, exist_ok=True)
207
+ def seed_module_metadata(self) -> None:
208
+ """Write a fresh metadata file containing only the module hashes.
207
209
 
210
+ Called at the start of a full render, before any functionality is
211
+ rendered, so change detection has a clean baseline.
212
+ """
213
+ metadata_utils.write_metadata(self.module_metadata_path(), self.get_hashes())
214
+
215
+ def truncate_metadata_functionalities(self, frid: str | None) -> None:
216
+ """Trim the stored functionalities list to the first int(frid) entries.
217
+
218
+ The metadata file is not tracked in the module's git repo, so when the
219
+ code repo is reverted to an earlier functionality the stored list must
220
+ be trimmed to match the reverted code. A frid of None means no
221
+ functionality is implemented (empty list).
222
+ """
223
+ metadata = self.load_module_metadata()
224
+ if metadata is None:
225
+ return
226
+
227
+ keep_count = int(frid) if frid is not None else 0
228
+ if metadata_utils.truncate_functionalities(metadata, keep_count):
229
+ metadata_utils.write_metadata(self.module_metadata_path(), metadata)
230
+
231
+ def revert_code_to_frid(self, frid: str | None) -> None:
232
+ """Revert the code repo to the commit for frid and keep metadata in sync."""
233
+ git_utils.revert_to_commit_with_frid(self.module_build_folder, frid)
234
+ self.truncate_metadata_functionalities(frid)
235
+
236
+ def reconcile_metadata_with_git(self) -> None:
237
+ """
238
+ Trim the metadata functionalities list to what the code repo actually committed.
239
+ """
240
+ module_name, frid = git_utils.get_last_rendered_functionality(self.module_build_folder)
241
+ own_frid = frid if module_name == self.module_name else None
242
+
243
+ self.truncate_metadata_functionalities(own_frid)
244
+
245
+ def save_module_metadata(self):
208
246
  module_metadata = self.get_hashes()
209
- metadata_path = self.module_metadata_path()
210
247
  module_metadata[MODULE_FUNCTIONALITIES] = self._get_module_functional_requirements()
211
248
 
212
249
  required_modules_functionalities = {}
@@ -216,8 +253,7 @@ class PlainModule:
216
253
  if required_modules_functionalities:
217
254
  module_metadata[REQUIRED_MODULES_FUNCTIONALITIES] = required_modules_functionalities
218
255
 
219
- with open(metadata_path, "w", encoding="utf-8") as f:
220
- json.dump(module_metadata, f, indent=4)
256
+ metadata_utils.write_metadata(self.module_metadata_path(), module_metadata)
221
257
 
222
258
  def _ensure_module_folders_exist(self, first_render_frid: str, render_conformance_tests: bool):
223
259
  """
@@ -372,10 +408,6 @@ class PlainModule:
372
408
  return False
373
409
 
374
410
  def wipe_module(self) -> None:
375
- if os.path.exists(self.module_build_folder):
376
- console.warning(f"Wiping module {self.module_build_folder}...")
377
- shutil.rmtree(self.module_build_folder)
378
-
379
- if os.path.exists(self.module_conformance_tests_folder):
380
- console.warning(f"Wiping conformance tests for module {self.module_conformance_tests_folder}...")
381
- shutil.rmtree(self.module_conformance_tests_folder)
411
+ if os.path.exists(self.module_folder):
412
+ console.warning(f"Wiping module {self.module_folder}...")
413
+ shutil.rmtree(self.module_folder)
@@ -1,4 +1,3 @@
1
- import json
2
1
  from typing import Any
3
2
 
4
3
  import file_utils
@@ -24,7 +23,7 @@ class PrepareRepositories(BaseAction):
24
23
 
25
24
  console.debug(f"Reverting code to version implemented for {previous_frid}.")
26
25
 
27
- git_utils.revert_to_commit_with_frid(render_context.build_folder, previous_frid)
26
+ render_context.plain_module.revert_code_to_frid(previous_frid)
28
27
  # conformance tests are still not fully implemented
29
28
  if render_context.render_conformance_tests:
30
29
  git_utils.revert_to_commit_with_frid(
@@ -33,22 +32,18 @@ class PrepareRepositories(BaseAction):
33
32
  )
34
33
 
35
34
  else:
36
- module_hashes = render_context.plain_module.get_hashes()
37
- initial_files = {
38
- render_context.plain_module.module_metadata_path(for_git_repo=True): json.dumps(module_hashes)
39
- }
35
+ file_utils.delete_folder(render_context.plain_module.module_folder)
36
+ render_context.plain_module.seed_module_metadata()
40
37
 
41
38
  if render_context.required_modules:
42
39
  previous_module = render_context.required_modules[-1]
43
40
  console.debug(f"Cloning git repo from module {previous_module.module_name}.")
44
41
 
45
- file_utils.delete_folder(render_context.build_folder)
46
42
  git_utils.clone_repo(
47
43
  previous_module.module_build_folder,
48
44
  render_context.build_folder,
49
45
  render_context.module_name,
50
46
  render_context.run_state.render_id,
51
- initial_files,
52
47
  )
53
48
  else:
54
49
  console.debug("Initializing git repositories for the render folders.")
@@ -57,7 +52,6 @@ class PrepareRepositories(BaseAction):
57
52
  render_context.build_folder,
58
53
  render_context.module_name,
59
54
  render_context.run_state.render_id,
60
- initial_files,
61
55
  )
62
56
 
63
57
  if render_context.base_folder:
@@ -75,7 +69,6 @@ class PrepareRepositories(BaseAction):
75
69
  render_context.conformance_tests.get_module_conformance_tests_folder(render_context.module_name),
76
70
  render_context.module_name,
77
71
  render_context.run_state.render_id,
78
- initial_files,
79
72
  )
80
73
 
81
74
  return self.SUCCESSFUL_OUTCOME, None