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]