Passing int value to vocabulary.getTerm() results in LookupError

Was scratching my head for a bit today before finding out what I had done wrong... Maybe this will help someone, and maybe a different error message "Hey dimwit, tokens must be ascii!" would have made me look in the right direction somewhat faster.

What had happened is that I unknowingly passed an INT to look for a term in my vocabulary. This resulted in the following Traceback:

Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 167, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 376, in publish_module
  Module ZPublisher.WSGIPublisher, line 271, in publish
  Module ZPublisher.mapply, line 85, in mapply
  Module ZPublisher.WSGIPublisher, line 68, in call_object
  Module pnz.erpediem.core.browser.invoice, line 50, in __call__
  Module plone.autoform.view, line 42, in __call__
  Module plone.autoform.view, line 33, in render
  Module Products.Five.browser.pagetemplatefile, line 126, in __call__
  Module Products.Five.browser.pagetemplatefile, line 58, in __call__
  Module zope.pagetemplate.pagetemplate, line 133, in pt_render
  Module Products.PageTemplates.engine, line 365, in __call__
  Module z3c.pt.pagetemplate, line 176, in render
  Module chameleon.zpt.template, line 302, in render
  Module chameleon.template, line 215, in render
  Module chameleon.utils, line 53, in raise_with_traceback
  Module chameleon.template, line 192, in render
  Module c0fc417e90455b08c29fb58c91916011, line 469, in render
  Module c39cf845968cb86048645198670b6234, line 428, in render_master
  Module zope.contentprovider.tales, line 76, in __call__
  Module zope.viewlet.manager, line 157, in update
  Module zope.viewlet.manager, line 163, in _updateViewlets
  Module plone.app.layout.viewlets.common, line 142, in update
  Module plone.memoize.view, line 59, in memogetter
  Module plone.app.layout.viewlets.common, line 129, in page_title
  Module plone.memoize.view, line 59, in memogetter
  Module plone.app.layout.globals.context, line 145, in object_title
  Module plone.base.utils, line 183, in pretty_title_or_id
  Module pnz.erpediem.core.trajects.invoice, line 57, in Title
  Module zope.schema.vocabulary, line 196, in getTerm
LookupError: 380

My invoice was supposed to show a Title based on the contents of the vocabulary:

    def Title(self):
        vocabulary = BGMDocumentCodesVocabulary(context=None)
        document_type = self._object.document_type
        document_type_title = translate(vocabulary.getTerm(document_type).title, context=getRequest())

        return u'%s (%s)' % (self._object.invoice_id, document_type_title)

Now, looking in my Vocabulary, I did find the value "380" and my SQLAlchemy-based CT object clearly used

document_type = Column(
    types.Unicode(3), 
    default=null(),
)

But of course, that code only matters when writing to MySQL...

Cause of the problem: in my MySQL database schema, I had stupidly defined the document_type column as an INT, so I received an INT and Plone barfed...

Knowing that Plone coerces tokens to always be ascii when writing them, shouldn't there have been some kind of sanity check in zope.schema.vocabulary to prevent me from passing on something else than an ascii string?

It doesn't matter to zope.schema.vocabulary what the value type specifically is, as long as you can use it as a key for the dictionary.

I'd guess you implemented BGMDocumentCodesVocabulary with str values. You can either not do that or do something like document_type = str(self._object.document_type).

re the sanity check: it could do something like assert value, str but that will only result in a runtime error like you have right now.

Wouldn't mypy catch this? It has a ZCA plugin, right?

Yes, that is all correct. I changed the column to a varchar in MySQL database schema and all was good.
But still wondering if a TypeError would not have been more appropriate, given:

plone.app.vocabularies: 4.2.1 → 4.2.2

Bug fixes:
Change vocabulary tokens to use base64.urlsafe_b64encode().
No newlines and safe to use as an xml attribute.
See community post <https://community.plone.org/t/tags-subject-field-mangling-long-terms/13067>_.
[flipmcf] (#64)

Yes, something like mypy should've caught that you created the vocab with only strings and are using it with ints.

The p.a.vocab urlsafe thing you refer to is only applicable to the token as that's what is passed around in forms, json and the like, not the value of the vocab item. There's is no type mismatch for the value, it's just Any.

How do you populate the BGMDocumentCodesVocabulary and how did you end up with strings in the vocab the first place? Don't you grab it from the DB and pass the values in as-is?

Nothing special about that vocabulary.

The vocab titles are po terms like this:

#. Default: "Commercial dispute"
#: ../vocabularies/edifact/vocabularies.py:22
msgid "value_bgm_067"
msgstr "Streitfall"

#. Default: "Credit note related to financial adjustments"
#: ../vocabularies/edifact/vocabularies.py:27
msgid "value_bgm_083"
msgstr "Bonifikationen"

from z3c.form.interfaces import NOVALUE
from zope.interface import provider
from zope.schema.interfaces import IVocabularyFactory
from zope.schema.vocabulary import SimpleTerm
from zope.schema.vocabulary import SimpleVocabulary
from pnz.erpediem.core import _


"""
67  Handelsunstimmigkeit (Reklamation)
83  Wertgutschrift
84  Wertbelastung
380 Handelsrechnung
381 Gutschriftsanzeige - Waren und Dienstleistungen
383 Belastungsanzeige - Waren und Dienstleistungen
386 Vorauszahlungsrechnung
393 Inkasso Rechnung (Rechnungsliste/ Sammelabrechnung)
"""
BGM_CODES = [
    (
        "67",
        _(
            "value_bgm_067",
            default="Commercial dispute",
        ),
    ),
    (
        "83",
        _(
            "value_bgm_083",
            default="Credit note related to financial adjustments",
        ),
    ),
    (
        "380",
        _(
            "value_bgm_380",
            default="Commercial invoice",
        ),
    ),
    (
        "381",
        _(
            "value_bgm_381",
            default="Credit note",
        ),
    ),
    (
        "383",
        _(
            "value_bgm_383",
            default="Debit note",
        ),
    ),
    (
        "393",
        _(
            "value_bgm_393",
            default="Invoice list",
        ),
    ),
]


@provider(IVocabularyFactory)
def BGMDocumentCodesVocabulary(context):
    """Provides a vocabulary of EDIFACT BGM document codes
    Full list including descriptions is here:
    https://service.unece.org/trade/untdid/d01b/tred/tred1001.htm
    """
    terms = [
        SimpleTerm(token=term[0], value=term[0], title=term[1]) for term in BGM_CODES
    ]
    return SimpleVocabulary(terms)

Ok, so the vocabulary uses "380" as value, because ^ file says so :slight_smile:

I think you know what you have to do :wink:

1 Like