Source code for omnipresence.hostmask

# -*- test-case-name: omnipresence.test.test_hostmask -*-
"""Operations on IRC hostmasks."""


from collections import namedtuple
import itertools
import re


def mask_as_regex(mask):
    """Return a regex object corresponding to the IRC hostmask pattern
    string *mask*."""
    pattern = ''
    backslash = False  # was the last character a backslash?
    for char in mask:
        if backslash:
            # The RFC doesn't state this explicitly, but the provided
            # BNF syntax defines the backslash as an escape character
            # only when it precedes a wildcard; otherwise, it's just a
            # backslash.  I don't really see this coming up much in
            # practice, since the characters '*', '?', and '\' don't
            # ever come up in real hostmasks, but it's best to follow
            # the rules as closely as possible.
            #
            # Technically, we parse incorrectly on masks like '\\*',
            # which should match a single backslash followed by an
            # asterisk, not two backslashes followed by any sequence of
            # characters, but someone who comes up with a mask like that
            # doesn't deserve to use this function.
            if char == '*' or char == '?':
                pattern += '\\' + char
            else:
                pattern += '\\\\' + re.escape(char)
            backslash = False
        else:
            if char == '\\':
                backslash = True
            elif char == '*':
                pattern += '.*'
            elif char == '?':
                pattern += '.'
            else:
                pattern += re.escape(char)
    # Anchoring the regex with '\A' and '\Z' ensures that the pattern
    # matches the entire string, not just a portion.
    return re.compile(r'\A' + pattern + r'\Z')


[docs]class Hostmask(namedtuple('Hostmask', ('nick', 'user', 'host'))): """Represents an IRC hostmask (sometimes called a message prefix) of the form ``nick!user@host``. The *user* and *host* attributes are optional, and default to `None` if not present.""" @classmethod
[docs] def from_string(cls, string): """Return a new `.Hostmask` object parsed from *string*, according to the definition of ``<prefix>`` in :rfc:`1459#section-2.3.1`.""" rest, _, host = string.partition('@') nick, _, user = rest.partition('!') return cls(nick, user or None, host or None)
[docs] def matches(self, other, case_mapping=None): """Check whether this hostmask matches the pattern in *other*, which can be a `Hostmask` object or a string in the form ``"nick!user@host"``, according to the wildcard expansion rules in :rfc:`2812#section-2.5`. A `.CaseMapping` object may optionally be provided, in which case nicks are compared case-insensitively according to the mapping's rules; otherwise, nick comparisons are fully case-sensitive. Users and hosts are always compared case-insensitively, using normal ASCII case folding rules. Briefly, ``*`` and ``?`` wildcards match zero or more and exactly one non-delimiter character, respectively; a backslash can be used to escape these special characters. Components that equal `None` are assumed to match all possible values for that component. .. warning:: This method only matches wildcards in *other*, not the `Hostmask` object it is called on:: >>> Hostmask.from_string('nick!user@host').matches('nick!*@*') True >>> Hostmask.from_string('nick!*@*').matches('nick!user@host') False """ # pylint: disable=invalid-name me = self if isinstance(other, str): other = Hostmask.from_string(other) me, other = ( x._replace( nick=case_mapping.lower(x.nick) if case_mapping else x.nick, user=x.user.lower() if x.user else None, host=x.host.lower() if x.host else None) for x in (me, other)) for mine, theirs in itertools.izip(me, other): if mine is None or theirs is None: continue if not mask_as_regex(theirs).match(mine): return False return True
def __str__(self): return (self.nick + (('!' + self.user) if self.user else '') + (('@' + self.host) if self.host else '')) @property def has_wildcard(self): """`True` if this hostmask contains a wildcard or any `None` components. Otherwise, `False` otherwise.""" if any(x is None for x in self): return True for component in self: backslash = False # was the last character a backslash? for char in component: if backslash: # Same caveat as in mask_as_regex. backslash = False else: if char == '\\': backslash = True elif char in ('*', '?'): return True return False