Repository: beetbox/beets

Issue:
%ifdef does not work on flexattr fields with int or float
### Problem
If I try `beet ls -a -f '$albumartist - (%if{$original_year,$original_year,$year}) - $album%ifdef{albumdisambig, (%title{$albumdisambig}),}%ifdef{albumrating, ($albumrating),}' year:2021` then beets will return the album rating for the albums that has the field defined. For albums without the rating, `$albumrating` is returned.

Example:
```
Artist1 - (2021) - Albumname1 (2.0)
Artist2 - (2021) - Albumname2 ($albumrating)
```

Further information on the forum:
https://discourse.beets.io/t/album-rating-as-a-flexible-attribute-in-5-increments/1687/3?u=pandatroubles

Comment from adrian:
_reading over the implementation of %ifdef, I can see now how it would be confused about types with a default value. That is, int or float fields will produce a '0' string for unset values through the formatter, which gets interpreted as non-missing. We clearly need better logic for this._

### Setup

* OS: Arch Linux
* beets version 1.5.0
* Python version 3.9.1
* plugins: edit, extrafiles, fetchart, importadded, importfeeds, info, inline, lastimport, mbcollection, mbsync, mpdstats, mpdupdate, originquery, scrub, smartplaylist, types, zero


Discussion:

Patch:
diff --git a/beets/library.py b/beets/library.py
index 6e13bf82c..dcd5a6a1f 100644
--- a/beets/library.py
+++ b/beets/library.py
@@ -1753,7 +1753,7 @@ class DefaultTemplateFunctions(object):
         :param falseval: The string if the condition is false
         :return: The string, based on condition
         """
-        if self.item.formatted().get(field):
+        if field in self.item:
             return trueval if trueval else self.item.formatted().get(field)
         else:
             return falseval
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 26cf39ee6..69e2f01a7 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -377,6 +377,9 @@ Fixes:
 * :doc`/reference/cli`: Remove reference to rarfile version in link
 * Fix :bug:`2873`. Duplicates can now generate checksums. Thanks user:`wisp3rwind`
   for the pointer to how to solve. Thanks to :user:`arogl`.
+* Templates that use ``%ifdef`` now produce the expected behavior when used in
+  conjunction with non-string fields from the :doc:`/plugins/types`.
+  :bug:`3852`
 
 
 For plugin developers:


Test patch:
diff --git a/test/test_types_plugin.py b/test/test_types_plugin.py
index 77d6c8bcf..65ad7bee4 100644
--- a/test/test_types_plugin.py
+++ b/test/test_types_plugin.py
@@ -145,6 +145,39 @@ class TypesPluginTest(unittest.TestCase, TestHelper):
         with self.assertRaises(ConfigValueError):
             self.run_command(u'ls')
 
+    def test_template_if_def(self):
+        # Tests for a subtle bug when using %ifdef in templates along with
+        # types that have truthy default values (e.g. '0', '0.0', 'False')
+        # https://github.com/beetbox/beets/issues/3852
+        self.config['types'] = {'playcount': u'int', 'rating': u'float',
+                                'starred': u'bool'}
+
+        with_fields = self.add_item(artist=u'prince')
+        self.modify(u'playcount=10', u'artist=prince')
+        self.modify(u'rating=5.0', u'artist=prince')
+        self.modify(u'starred=yes', u'artist=prince')
+        with_fields.load()
+
+        without_fields = self.add_item(artist=u'britney')
+
+        int_template = u'%ifdef{playcount,Play count: $playcount,Not played}'
+        self.assertEqual(with_fields.evaluate_template(int_template),
+                         u'Play count: 10')
+        self.assertEqual(without_fields.evaluate_template(int_template),
+                         u'Not played')
+
+        float_template = u'%ifdef{rating,Rating: $rating,Not rated}'
+        self.assertEqual(with_fields.evaluate_template(float_template),
+                         u'Rating: 5.0')
+        self.assertEqual(without_fields.evaluate_template(float_template),
+                         u'Not rated')
+
+        bool_template = u'%ifdef{starred,Starred: $starred,Not starred}'
+        self.assertIn(with_fields.evaluate_template(bool_template).lower(),
+                      (u'starred: true', u'starred: yes', u'starred: y'))
+        self.assertEqual(without_fields.evaluate_template(bool_template),
+                         u'Not starred')
+
     def modify(self, *args):
         return self.run_with_output(u'modify', u'--yes', u'--nowrite',
                                     u'--nomove', *args)