Changing tags in a plugin? Help please

56 views
Skip to first unread message

stalbansrf...@gmail.com

unread,
Feb 16, 2017, 5:22:35 AM2/16/17
to Beancount
Hi
I am writing a plugin to flag transactions that have specified accounts ('cash') with a 'CASH' tag.
I can't work out how to amend a tag - I have copied code from an example plugin but it doesn't do anything - the print shows both tags as the same.
The code is below - but I also tried doing a entry._replace(tags={'CASH'}) doesn't work, or entry.tags={'CASH'}. or entry.tags=fixedset({'CASH'})
I can't see any way to manipulate tags in beancount.core.data.
Any help please?
Regards
Paul


    new_entries = []
    for entry in entries:
        if isinstance(entry, data.Transaction):
            orig_entry = entry
            for posting in entry.postings:
                 if (posting.account in accounts):
                     entry._replace(tags=(entry.tags or set()) | set(('CASH',)))
                     print (orig_entry.tags, entry.tags)
        new_entries.append(entry)
#        print (entry)
    return new_entries, []

==========================



"""
For example, a possible configuration could be:
  plugin "flagcash" "{
 'accounts': {
              'Assets:Cash:Bank',
       'Assets:Cash:Float' }
  }"

"""
__author__ = 'Paul Hamshere'
#from beancount.core.number import MISSING
from beancount.core import data
from beancount.core import account_types
#from beancount.core import amount
#from beancount.parser import printer

__plugins__ = ['flagcash']

DEBUG = 0

def flagcash(entries, options_map, config):
    """
    Args:
      entries: a list of entry instances
      options_map: a dict of options parsed from the file
      config: A configuration string, which is intended to be a Python dict
    Returns:
      A tuple of entries and errors.
    """
    # Parse and extract configuration values.
    config_obj = eval(config, {}, {})
    if not isinstance(config_obj, dict):
        raise RuntimeError("Invalid plugin configuration: should be a single dict.")

    accounts = config_obj.pop('accounts', {})
    new_entries = []
    for entry in entries:
        if isinstance(entry, data.Transaction):
            orig_entry = entry
            for posting in entry.postings:
                 if (posting.account in accounts):
                     entry._replace(tags=(entry.tags or set()) | set(('CASH',)))
                     print (orig_entry.tags, entry.tags)
        new_entries.append(entry)
#        print (entry)
    return new_entries, []

p.w.ha...@gmail.com

unread,
Feb 17, 2017, 2:12:41 PM2/17/17
to Beancount
Oops my error should have put

entries = entries._replace.....

Paul

stalbansrf...@gmail.com

unread,
Feb 18, 2017, 5:04:21 AM2/18/17
to Beancount, p.w.ha...@gmail.com
Now it works.
This can flag cash accounts, but is now configurable to flag any transaction what contains any of the listed accounts.

I still don't understand how this works, or what type an empty tag set is.... (tagset or set()) | set((tagged,))

Sample header in journal file:

plugin "beancount.plugins.auto_accounts"
plugin "tagaccount" "{
        'accounts': {
              'Assets:Cash:RBS',
              'Assets:Cash:MinisFloat',
              'Assets:Cash:NatWest' },
        'tag': 'cash'
  }"

Code:

"""

 

For example, a possible configuration could be:

 

plugin "tagaccount`" "{

'accounts': {

'Assets:Cash:Bank',

'Assets:Cash:Float' },

'tag': 'CASH'

}"

"""

 

__author__ = 'Paul Hamshere'

 

from beancount.core import data

from beancount.core import account_types

 

__plugins__ = ['tagaccount']

 

def tagaccount(entries, options_map, config):

"""

Args:

entries: a list of entry instances

options_map: a dict of options parsed from the file

config: A configuration string, which is intended to be a Python dict

Returns:

A tuple of entries and errors.

"""

 

# Parse and extract configuration values.

 

config_obj = eval(config, {}, {})

if not isinstance(config_obj, dict):

     raise RuntimeError("Invalid plugin configuration: should be a single dict.")

 

accounts = config_obj.pop('accounts', {})

tagged = config_obj.pop('tag',{})

new_entries = []

for entry in entries:

     if isinstance(entry, data.Transaction):

         orig_entry = entry

         for posting in entry.postings:

              if (posting.account in accounts):

 

                    tagset = entry.tags

                    tagset = (tagset or set()) | set((tagged,))

                    entry = entry._replace(tags=tagset)

      new_entries.append(entry)

return new_entries, []


Code completely adapted from the example plugin.
Regards
Paul 

Martin Blais

unread,
Feb 18, 2017, 9:47:16 AM2/18/17
to Beancount, p.w.ha...@gmail.com
entry.tags is supposed to be a set.
If not set, an empty set.
I reuse a constant in the codebase (see EMPTY_SET).

TBH I'm not 100% sure what your question is.




--
You received this message because you are subscribed to the Google Groups "Beancount" group.
To unsubscribe from this group and stop receiving emails from it, send an email to beancount+unsubscribe@googlegroups.com.
To post to this group, send email to bean...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/beancount/68c72034-53d7-4f1c-91f2-3a66ec32aaab%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

stalbansrf...@gmail.com

unread,
Feb 18, 2017, 9:59:22 AM2/18/17
to Beancount, p.w.ha...@gmail.com
Martin
Thanks.
My question was all around how to test for an empty tag and update it - your code does this with

(tagset or set()) | set((tagged,))

I tried to do a logic test on if tagset == None:   and also  if tagset == set():    but neither seemed to work.
I was going to do

if tagset == None:
   tagset = set(tagged,)
else:
   tagset = tagset | set (tagged,)

But failed. So I wondered what you had as your 'empty set'. I'll have a look for EMPTY_SET.

Not sure if my adaptation of your example is of use to anyone.

Regards
Paul
To unsubscribe from this group and stop receiving emails from it, send an email to beancount+...@googlegroups.com.

To post to this group, send email to bean...@googlegroups.com.

Martin Blais

unread,
Feb 18, 2017, 4:23:48 PM2/18/17
to Beancount, p.w.ha...@gmail.com
There's a bit of history there: an empty 'tags' attribute used to be set to None.
This is mostly internal changes, but I suppose if you write a plugin it matters.
I made them guaranteed to always be of a "set" type.
In order to remain efficient, when empty, it is set to the same empty frozenset instance.
That set referenced by beancount.core.data.EMPTY_SET

(tagset or set()) | set((tagged,)) is a one-liner that will handle both the None or empty set case during the transition.

You should be able to assume it's a set object, and do ....tags = entry.tags | set((tagged,))

Now, there's a second aspect to this that was throwing you off: I treat those objects as immutable.
That's why you have to use the _replace() method to produce a new object, the namedtuple is not mutable, and while its 'tags' attribute is (if it is a set), it is not if it is a frozenset. So you can't mutate in-place.
That's just a better style of programming; it might be a tad uglier in Python, but avoiding side-effects avoids a lot of other problems, though I think in the case of plugins, nothing else holds onto those objects so in theory they could have been mutated in-place without harm.



To unsubscribe from this group and stop receiving emails from it, send an email to beancount+unsubscribe@googlegroups.com.

To post to this group, send email to bean...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages