# coding=utf-8
from __future__ import unicode_literals, absolute_import, print_function, division

import functools
from sopel.tools import Identifier


@functools.total_ordering
class User(object):
    """A representation of a user Sopel is aware of."""
    def __init__(self, nick, user, host):
        assert isinstance(nick, Identifier)
        self.nick = nick
        """The user's nickname."""
        self.user = user
        """The user's local username."""
        self.host = host
        """The user's hostname."""
        self.channels = {}
        """The channels the user is in.

        This maps channel name ``Identifier``\\s to ``Channel`` objects."""
        self.account = None
        """The IRC services account of the user.

        This relies on IRCv3 account tracking being enabled."""
        self.away = None
        """Whether the user is marked as away."""

    hostmask = property(lambda self: '{}!{}@{}'.format(self.nick, self.user,
                                                       self.host))
    """The user's full hostmask."""

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.nick == other.nick

    def __lt__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.nick < other.nick


@functools.total_ordering
class Channel(object):
    """A representation of a channel Sopel is in."""
    def __init__(self, name):
        assert isinstance(name, Identifier)
        self.name = name
        """The name of the channel."""
        self.users = {}
        """The users in the channel.

        This maps username ``Identifier``\\s to channel objects."""
        self.privileges = {}
        """The permissions of the users in the channel.

        This maps username ``Identifier``s to bitwise integer values. This can
        be compared to appropriate constants from ``sopel.module``."""
        self.topic = ''
        """The topic of the channel."""

    def clear_user(self, nick):
        user = self.users.pop(nick, None)
        self.privileges.pop(nick, None)
        if user is not None:
            user.channels.pop(self.name, None)

    def add_user(self, user):
        assert isinstance(user, User)
        self.users[user.nick] = user
        self.privileges[user.nick] = 0
        user.channels[self.name] = self

    def rename_user(self, old, new):
        if old in self.users:
            self.users[new] = self.users.pop(old)
        if old in self.privileges:
            self.privileges[new] = self.privileges.pop(old)

    def __eq__(self, other):
        if not isinstance(other, Channel):
            return NotImplemented
        return self.name == other.name

    def __lt__(self, other):
        if not isinstance(other, Channel):
            return NotImplemented
        return self.name < other.name

sopel/tools/target.py
# coding=utf-8
from __future__ import unicode_literals, absolute_import, print_function, division

import functools
from sopel.tools import Identifier


@functools.total_ordering
class User(object):
    """A representation of a user Sopel is aware of."""
    def __init__(self, nick, user, host):
        assert isinstance(nick, Identifier)
        self.nick = nick
        """The user's nickname."""
        self.user = user
        """The user's local username."""
        self.host = host
        """The user's hostname."""
        self.channels = {}
        """The channels the user is in.

        This maps channel name ``Identifier``\\s to ``Channel`` objects."""
        self.account = None
        """The IRC services account of the user.

        This relies on IRCv3 account tracking being enabled."""
        self.away = None
        """Whether the user is marked as away."""

    hostmask = property(lambda self: '{}!{}@{}'.format(self.nick, self.user,
                                                       self.host))
    """The user's full hostmask."""

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.nick == other.nick

    def __lt__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.nick < other.nick


@functools.total_ordering
class Channel(object):
    """A representation of a channel Sopel is in."""
    def __init__(self, name):
        assert isinstance(name, Identifier)
        self.name = name
        """The name of the channel."""
        self.users = {}
        """The users in the channel.

        This maps username ``Identifier``\\s to channel objects."""
        self.privileges = {}
        """The permissions of the users in the channel.

        This maps username ``Identifier``s to bitwise integer values. This can
        be compared to appropriate constants from ``sopel.module``."""
        self.topic = ''
        """The topic of the channel."""

    def clear_user(self, nick):
        user = self.users.pop(nick, None)
        self.privileges.pop(nick, None)
        if user is not None:
            user.channels.pop(self.name, None)

    def add_user(self, user):
        assert isinstance(user, User)
        self.users[user.nick] = user
        self.privileges[user.nick] = 0
        user.channels[self.name] = self

    def rename_user(self, old, new):
        if old in self.users:
            self.users[new] = self.users.pop(old)
            self.users[new].nick = new
        if old in self.privileges:
            self.privileges[new] = self.privileges.pop(old)

    def __eq__(self, other):
        if not isinstance(other, Channel):
            return NotImplemented
        return self.name == other.name

    def __lt__(self, other):
        if not isinstance(other, Channel):
            return NotImplemented
        return self.name < other.name

sopel/tools/target.py
diff --git a/sopel/tools/target.py b/sopel/tools/target.py
index a5e73a1595..1347ea0324 100644
--- a/sopel/tools/target.py
+++ b/sopel/tools/target.py
@@ -76,6 +76,7 @@ def add_user(self, user):
     def rename_user(self, old, new):
         if old in self.users:
             self.users[new] = self.users.pop(old)
+            self.users[new].nick = new
         if old in self.privileges:
             self.privileges[new] = self.privileges.pop(old)
 

User objects aren't updated on NICK events
````
<Reptarsaurus> Okay, I just tested....I am doing users = chans[chan].users.values() after
               a nick change, and 'for user in users: print(user.nick)' is showing the
               nick pre-change.
````

This report from Sopel's IRC channel told me something was off in the code that keeps track of users, and indeed something is. Nowhere in the `coretasks` handler for `NICK` events does Sopel update the user object's `nick` property:

https://github.com/sopel-irc/sopel/blob/d4c4c16f850b63776b156fc8c9d3812287b15882/sopel/coretasks.py#L275-L284

`Channel.rename_user()` doesn't touch it either:

https://github.com/sopel-irc/sopel/blob/d4c4c16f850b63776b156fc8c9d3812287b15882/sopel/tools/target.py#L76-L80

I can't find any place in the code where this property gets updated, ever, and that's a bug. Thanks to Reptarsaurus for stumbling onto this, and it should be fixed in the next Sopel version.
