From 1adedd6adcf4b112706f75ff64c5b1b02f0fba7d Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 27 Jul 2026 22:18:57 +1000 Subject: [PATCH] Split export state into a read-only merged `exported` and a host-owned `meta_exported` switch --- aidialog/_modidx.py | 3 ++- aidialog/dialog.py | 21 ++++++++++++--------- nbs/01_dialog.ipynb | 39 ++++++++++++++++++++------------------- nbs/03_dlgskill.ipynb | 2 +- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/aidialog/_modidx.py b/aidialog/_modidx.py index a10c982..dd7a585 100644 --- a/aidialog/_modidx.py +++ b/aidialog/_modidx.py @@ -64,8 +64,9 @@ 'aidialog.dialog._code_span': ('dialog.html#_code_span', 'aidialog/dialog.py'), 'aidialog.dialog._fmt_param': ('dialog.html#_fmt_param', 'aidialog/dialog.py'), 'aidialog.dialog._get_exported': ('dialog.html#_get_exported', 'aidialog/dialog.py'), + 'aidialog.dialog._get_mexp': ('dialog.html#_get_mexp', 'aidialog/dialog.py'), 'aidialog.dialog._latex_parts': ('dialog.html#_latex_parts', 'aidialog/dialog.py'), - 'aidialog.dialog._set_exported': ('dialog.html#_set_exported', 'aidialog/dialog.py'), + 'aidialog.dialog._set_mexp': ('dialog.html#_set_mexp', 'aidialog/dialog.py'), 'aidialog.dialog.add_id_hash': ('dialog.html#add_id_hash', 'aidialog/dialog.py'), 'aidialog.dialog.ai_fmt': ('dialog.html#ai_fmt', 'aidialog/dialog.py'), 'aidialog.dialog.code_output': ('dialog.html#code_output', 'aidialog/dialog.py'), diff --git a/aidialog/dialog.py b/aidialog/dialog.py index 72a7357..0d9822e 100644 --- a/aidialog/dialog.py +++ b/aidialog/dialog.py @@ -482,15 +482,18 @@ def directives(self:Message): return mk_cell(self.content, metadata=self.meta).directives def _get_exported(self): return 'export' in self.directives or 'exports' in self.directives -def _set_exported(self, v): - c = mk_cell(self.content, metadata=self.meta) - d = c.directives - val = d.get('export', d.get('exports')) - c.directives = {k:x for k,x in d.items() if k not in ('export','exports')} - if self.content != c.source: self.content = c.source - if v: self.meta.setdefault('nbdev',{})['export'] = val or 'true' -Message.exported = property(_get_exported, _set_exported, - doc="Whether this message carries an nbdev export directive, in content or meta; assigning writes the meta form, migrating any content directive") +Message.exported = property(_get_exported, + doc="Whether this message carries an nbdev export directive, in content or meta (read-only: edit content, or assign `meta_exported`)") + +def _get_mexp(self): return any(k in self.meta.get('nbdev',{}) for k in ('export','exports')) +def _set_mexp(self, v): + if v: self.meta.setdefault('nbdev',{})['export'] = 'true' + else: + nbd = self.meta.get('nbdev',{}) + for k in ('export','exports'): nbd.pop(k, None) + if not nbd: self.meta.pop('nbdev', None) +Message.meta_exported = property(_get_mexp, _set_mexp, + doc="The meta `nbdev` export entry alone - the host-owned switch; assigning writes or clears only meta, never content") def dlg2py(dlg): "The exported code of `dlg`, as a python source string" diff --git a/nbs/01_dialog.ipynb b/nbs/01_dialog.ipynb index 00e2f3e..a4c93fa 100644 --- a/nbs/01_dialog.ipynb +++ b/nbs/01_dialog.ipynb @@ -1521,7 +1521,7 @@ "id": "59fcb413", "metadata": {}, "source": [ - "A message is exported when it carries an nbdev `export` directive - either a `#| export` comment in the content, or an `export` entry in the meta `nbdev` dict. The `directives` property merges both sources with comments winning, matching nbdev's own rule, so `exported` and every consumer of it see one answer. Assigning `exported` writes the canonical meta form: turning it on sets `meta['nbdev']['export']` and removes any content directive line (so re-toggling a message migrates it), turning it off clears both locations - no partial state survives a toggle. `dlg2py` joins a dialog's exported code into a python source string - the projection behind \"export to .py\" features." + "A message is exported when it carries an nbdev `export` directive - either a `#| export` comment in the content, or an `export` entry in the meta `nbdev` dict. The `directives` property merges both sources with comments winning, matching nbdev's own rule, so `exported` and every consumer of it see one answer. `exported` is read-only, because the two sources have different owners: a content directive is text the user owns and edits directly, while `meta_exported` is the host's own read-write switch - assigning it writes or clears only `meta['nbdev']`, never touching content. `dlg2py` joins a dialog's exported code into a python source string - the projection behind \"export to .py\" features." ] }, { @@ -1538,15 +1538,18 @@ " return mk_cell(self.content, metadata=self.meta).directives\n", "\n", "def _get_exported(self): return 'export' in self.directives or 'exports' in self.directives\n", - "def _set_exported(self, v):\n", - " c = mk_cell(self.content, metadata=self.meta)\n", - " d = c.directives\n", - " val = d.get('export', d.get('exports'))\n", - " c.directives = {k:x for k,x in d.items() if k not in ('export','exports')}\n", - " if self.content != c.source: self.content = c.source\n", - " if v: self.meta.setdefault('nbdev',{})['export'] = val or 'true'\n", - "Message.exported = property(_get_exported, _set_exported,\n", - " doc=\"Whether this message carries an nbdev export directive, in content or meta; assigning writes the meta form, migrating any content directive\")\n", + "Message.exported = property(_get_exported,\n", + " doc=\"Whether this message carries an nbdev export directive, in content or meta (read-only: edit content, or assign `meta_exported`)\")\n", + "\n", + "def _get_mexp(self): return any(k in self.meta.get('nbdev',{}) for k in ('export','exports'))\n", + "def _set_mexp(self, v):\n", + " if v: self.meta.setdefault('nbdev',{})['export'] = 'true'\n", + " else:\n", + " nbd = self.meta.get('nbdev',{})\n", + " for k in ('export','exports'): nbd.pop(k, None)\n", + " if not nbd: self.meta.pop('nbdev', None)\n", + "Message.meta_exported = property(_get_mexp, _set_mexp,\n", + " doc=\"The meta `nbdev` export entry alone - the host-owned switch; assigning writes or clears only meta, never content\")\n", "\n", "def dlg2py(dlg):\n", " \"The exported code of `dlg`, as a python source string\"\n", @@ -1566,19 +1569,17 @@ "e3 = xd.mk_message('def h(): pass', msg_type=scode, meta={'nbdev': {'export': 'true'}})\n", "xd.mk_message('#| export\\nexported note', msg_type=snote)\n", "test_eq([m.exported for m in xd.messages], [True, False, True, True]) # comment, none, meta, comment\n", + "test_eq([m.meta_exported for m in xd.messages], [False, False, True, False]) # the switch sees only meta\n", "test_eq(e1.directives, {'export': ''})\n", - "e2.exported = True # new exports go to meta, not content\n", + "e2.meta_exported = True # the switch writes meta, not content\n", "test_eq((e2.content, e2.meta), ('def g(): pass', {'nbdev': {'export': 'true'}}))\n", "test_eq(dlg2py(xd), '#| export\\ndef f(): pass\\n\\ndef g(): pass\\n\\ndef h(): pass') # code only\n", - "e2.exported = False # off clears meta\n", + "e2.meta_exported = False # off clears meta only\n", "test_eq((e2.exported, e2.meta), (False, {}))\n", - "e1.exported = True # True on a comment-exported message migrates the directive to meta\n", - "test_eq((e1.content, e1.meta), ('def f(): pass', {'nbdev': {'export': 'true'}}))\n", - "e1.exported = False\n", - "test_eq((e1.exported, e1.content, e1.meta), (False, 'def f(): pass', {}))\n", - "em = xd.mk_message('#|exports: mymod\\ndef i(): pass', msg_type=scode)\n", - "em.exported = True # migration keeps the directive's value, whatever its spelling\n", - "test_eq((em.content, em.meta), ('def i(): pass', {'nbdev': {'export': 'mymod'}}))" + "e1.meta_exported = True # a comment-exported message gains the meta form too; content untouched\n", + "test_eq((e1.content, e1.meta), ('#| export\\ndef f(): pass', {'nbdev': {'export': 'true'}}))\n", + "e1.meta_exported = False # still exported: the comment is text the user owns\n", + "test_eq((e1.exported, e1.meta_exported, e1.content, e1.meta), (True, False, '#| export\\ndef f(): pass', {}))" ] }, { diff --git a/nbs/03_dlgskill.ipynb b/nbs/03_dlgskill.ipynb index 7b10d33..7c39406 100644 --- a/nbs/03_dlgskill.ipynb +++ b/nbs/03_dlgskill.ipynb @@ -481,7 +481,7 @@ "xd = Dialog(name='exports')\n", "exp1 = xd.mk_message('#| export\\ndef f(): pass', msg_type=scode)\n", "exp2 = xd.mk_message('def g(): pass', msg_type=scode)\n", - "exp2.exported = True\n", + "exp2.meta_exported = True\n", "deep = xd.mk_message('## Deep\\nbody under the heading', msg_type=snote)\n", "test_eq(xd.find_msgs(only_exp=True, context=0), [exp1, exp2])\n", "test_eq(xd.find_msgs(header_section='Deep', context=0), [deep]) # matched on the first line only\n",