Xqt has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1358463?usp=email )

Change subject: lexeme: Enable creation through editEntity
......................................................................

lexeme: Enable creation through editEntity

Inherit empty creation defaults so wbeditentity accepts new Lexemes
from explicit data or populated objects without replacing caller fields.
Reuse identity assignment and post-save reloading for subsequent edits.

Bug: T442295
Change-Id: Id2770c4cf1a539cc05165c5f8acdad14929dcbee
---
M docs/library_usage.rst
M pywikibot/page/_wikibase.py
M tests/wikibase_tests.py
3 files changed, 152 insertions(+), 5 deletions(-)

Approvals:
  Xqt: Verified; Looks good to me, approved




diff --git a/docs/library_usage.rst b/docs/library_usage.rst
index 839c1e4..6c75bc5 100644
--- a/docs/library_usage.rst
+++ b/docs/library_usage.rst
@@ -105,3 +105,70 @@

 .. note::
    Also see the documentation at :manpage:`Create your own script`
+
+
+Creating Lexemes
+================
+
+A new :class:`pywikibot.LexemePage` can be saved with either an explicit
+creation dictionary or fields populated on the object. Wikibase requires
+at least one lemma, a language item and a lexical category item. The server
+validates these fields and any optional forms, senses and statements.
+
+The examples below assume ``repo`` is your configured Wikibase repository
+with WikibaseLexeme installed, and ``language_id`` and ``category_id`` are
+existing item IDs in that repository. Saving writes to that repository.
+
+To supply an explicit creation dictionary:
+
+.. code-block:: python
+
+    import pywikibot
+
+    lexeme = pywikibot.LexemePage(repo)
+    lexeme.editEntity({
+        'lemmas': {'en': 'colour'},
+        'language': language_id,
+        'lexicalCategory': category_id,
+        'forms': [{
+            'add': '',
+            'representations': {'en': 'colours'},
+            'grammaticalFeatures': [],
+        }],
+        'senses': [{'add': '', 'glosses': {'en': 'visual appearance'}}],
+    }, summary='Create a Lexeme')
+    print(lexeme.id)
+
+Explicit data takes precedence: fields already set on the object are not
+merged into the supplied dictionary. New forms and senses in an explicit
+operation list require the ``add`` marker.
+
+Alternatively, populate the object and save without a data argument:
+
+.. code-block:: python
+
+    lexeme = pywikibot.LexemePage(repo)
+    lexeme.lemmas['en'] = 'colour'
+    lexeme.language = pywikibot.ItemPage(repo, language_id)
+    lexeme.lexicalCategory = pywikibot.ItemPage(repo, category_id)
+    form = pywikibot.LexemeForm(repo)
+    form.representations['en'] = 'colours'
+    lexeme.forms.append(form)
+    sense = pywikibot.LexemeSense(repo)
+    sense.glosses['en'] = 'visual appearance'
+    lexeme.senses.append(sense)
+    lexeme.editEntity(summary='Create a Lexeme')
+
+After a successful save, ``lexeme.id`` contains the assigned Lexeme ID.
+Cached fields are invalidated and reloaded on access. Reacquire forms,
+senses and claims from the parent to obtain their server-assigned IDs;
+previously retained child objects remain snapshots. For example, use
+``form = lexeme.forms[0]`` before editing that form again. Subsequent calls
+to ``lexeme.editEntity()`` update the existing Lexeme.
+
+A rejected creation leaves local fields available for correction. A lost
+connection or timeout can instead leave the outcome unknown: the server
+may have created the Lexeme even though no response arrived. Check the
+repository before attempting another creation; retrying blindly can create
+a duplicate. Matching lemmas alone do not establish an entry's identity.
+Automatic recovery from uncertain write outcomes is not provided.
diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py
index 20c4976..fa8f5d3 100644
--- a/pywikibot/page/_wikibase.py
+++ b/pywikibot/page/_wikibase.py
@@ -2364,7 +2364,11 @@
     def editEntity(
         self, data: ENTITY_DATA_TYPE | None = None, **kwargs
     ) -> None:
-        """Edit the Lexeme and invalidate its cached data on success.
+        """Create or edit the Lexeme and invalidate cached data on success.
+
+        New Lexemes require lemmas, language and lexicalCategory. Pass
+        explicit API data or populate the Lexeme before saving. Explicit
+        data is not combined with local fields.

         Forms, senses and claims must be reacquired from this Lexeme after
         saving. Existing references are snapshots and are not updated with
@@ -2386,10 +2390,6 @@
         self.latest_revision_id = data.get('lastrevid')
         self._needs_reload = True

-    def get_data_for_new_entity(self) -> NoReturn:
-        """Return data required for creation of a new lexeme."""
-        raise NotImplementedError  # TODO
-
     def toJSON(self, diffto: dict | None = None) -> dict:
         """Create JSON suitable for Wikibase API.

diff --git a/tests/wikibase_tests.py b/tests/wikibase_tests.py
index fc7ca27..eb0c6f9 100755
--- a/tests/wikibase_tests.py
+++ b/tests/wikibase_tests.py
@@ -16,6 +16,7 @@
 import pywikibot
 from pywikibot import pagegenerators
 from pywikibot.exceptions import (
+    APIError,
     InvalidTitleError,
     IsNotRedirectPageError,
     IsRedirectPageError,
@@ -1245,6 +1246,7 @@
             ('_entity_namespaces', {'item': namespaces[0],
                                     'lexeme': namespace}),
             ('entity_sources', MagicMock(return_value={})),
+            ('_userinfo', {'rights': ['edit']}),
         ):
             patcher = patch.object(self.repo, attribute, value, create=True)
             patcher.start()
@@ -1335,6 +1337,84 @@
             save.assert_not_called()
         self.assertTrue(lexeme._needs_reload)

+    def test_create(self) -> None:
+        """Create from local fields and reuse assigned IDs on another save."""
+        lexeme = LexemePage(self.repo)
+        lexeme.lemmas['en'] = 'colour'
+        lexeme.language = ItemPage(self.repo, 'Q1860')
+        lexeme.lexicalCategory = ItemPage(self.repo, 'Q1084')
+        form = LexemeForm(self.repo)
+        form.representations['en'] = 'colours'
+        lexeme.forms.append(form)
+        response = copy.deepcopy(self.content)
+        response['forms'] = [dict(form.toJSON(), id='L1-F1')]
+        with patch.object(self.repo, '_tokens', {'csrf': 'token'}), \
+                patch.object(self.repo, 'simple_request') as request, \
+                patch.object(self.repo, 'loadcontent',
+                             side_effect=lambda *args: {
+                                 'L1': copy.deepcopy(response)}):
+            request.return_value.submit.return_value = {'entity': response}
+            lexeme.editEntity()
+            params = request.call_args.kwargs
+            self.assertEqual(params['action'], 'wbeditentity')
+            self.assertEqual(params['new'], 'lexeme')
+            self.assertNotIn('id', params)
+            payload = json.loads(params['data'])
+            self.assertEqual(payload['language'], 'Q1860')
+            self.assertEqual(payload['lexicalCategory'], 'Q1084')
+            self.assertEqual(payload['lemmas'], response['lemmas'])
+            self.assertEqual(payload['forms'], [dict(form.toJSON(), add='')])
+            self.assertEqual(lexeme.id, 'L1')
+            self.assertEqual(lexeme.latest_revision_id, 1)
+            lexeme.editEntity()
+            params = request.call_args.kwargs
+            self.assertEqual(params['id'], 'L1')
+            self.assertNotIn('new', params)
+            self.assertEqual(params['baserevid'], 1)
+            self.assertEqual(json.loads(params['data']), {})
+            self.assertEqual(lexeme.forms[0].id, 'L1-F1')
+            self.assertEqual(form.id, '-1')
+
+    def test_create_explicit(self) -> None:
+        """Explicit creation data wins over local fields; callbacks see IDs."""
+        lexeme = LexemePage(self.repo)
+        lexeme.lemmas['en'] = 'unused local lemma'
+        lexeme.language = ItemPage(self.repo, 'Q1')
+        data = {'lemmas': {'en': 'colour'}, 'language': 'Q1860',
+                'lexicalCategory': 'Q1084'}
+        original = copy.deepcopy(data)
+        callback = MagicMock()
+        with patch.object(self.repo, '_tokens', {'csrf': 'token'}), \
+                patch.object(self.repo, 'simple_request') as request, \
+                patch.object(pywikibot, 'async_request',
+                             side_effect=lambda func, *args, **kwargs:
+                             func(*args, **kwargs)):
+            request.return_value.submit.return_value = {'entity': self.content}
+            lexeme.editEntity(data, asynchronous=True, callback=callback)
+        self.assertEqual(request.call_args.kwargs['new'], 'lexeme')
+        self.assertEqual(json.loads(request.call_args.kwargs['data']), {
+            **original, 'lemmas': self.content['lemmas']})
+        self.assertEqual(data, original)
+        self.assertEqual(lexeme.id, 'L1')
+        self.assertNotIn('lemmas', lexeme.__dict__)
+        callback.assert_called_once_with(lexeme, None)
+
+    def test_create_rejected(self) -> None:
+        """Rejected creation preserves unsaved data and reports the error."""
+        lexeme = LexemePage(self.repo)
+        lexeme.lemmas['en'] = 'colour'
+        error = APIError('missing-field', 'language is required')
+        callback = MagicMock()
+        with patch.object(self.repo, '_tokens', {'csrf': 'token'}), \
+                patch.object(self.repo, 'simple_request') as request:
+            request.return_value.submit.side_effect = error
+            lexeme.editEntity(callback=callback)
+        self.assertEqual(lexeme.id, '-1')
+        self.assertEqual(lexeme.lemmas, {'en': 'colour'})
+        callback.assert_called_once()
+        self.assertIs(callback.call_args.args[0], lexeme)
+        self.assertIn('missing-field', str(callback.call_args.args[1]))
+
     def test_item_response(self) -> None:
         """Other entities still receive statement IDs from edit responses."""
         item = ItemPage(self.repo, 'Q1')

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1358463?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings?usp=email

Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Id2770c4cf1a539cc05165c5f8acdad14929dcbee
Gerrit-Change-Number: 1358463
Gerrit-PatchSet: 2
Gerrit-Owner: Mahveotm <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to