Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 140 additions & 36 deletions sopel/modules/pronouns.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,129 @@
"""
from __future__ import generator_stop

import logging

import requests

from sopel import plugin
from sopel.config import types


LOGGER = logging.getLogger(__name__)


class PronounsSection(types.StaticSection):
fetch_complete_list = types.BooleanAttribute('fetch_complete_list', default=True)
"""Whether to attempt fetching the complete list pronoun.is uses, at bot startup."""


def configure(settings):
"""
| name | example | purpose |
| ---- | ------- | ------- |
| fetch_complete_list | True | Whether to attempt fetching the complete pronoun list from pronoun.is at startup. |
"""
settings.define_section('pronouns', PronounsSection)
settings.pronouns.configure_setting(
'fetch_complete_list',
'Fetch the current pronoun.is list at startup?')


# Copied from pronoun.is, leaving a *lot* out. If
# https://github.com/witch-house/pronoun.is/pull/96 gets merged, using that
# would be a lot easier.
# If ambiguous, the earlier one will be used.
KNOWN_SETS = {
"ze/hir": "ze/hir/hir/hirs/hirself",
"ze/zir": "ze/zir/zir/zirs/zirself",
"they/.../themselves": "they/them/their/theirs/themselves",
"they/.../themself": "they/them/their/theirs/themself",
"she/her": "she/her/her/hers/herself",
"he/him": "he/him/his/his/himself",
"xey/xem": "xey/xem/xyr/xyrs/xemself",
"sie/hir": "sie/hir/hir/hirs/hirself",
"it/it": "it/it/its/its/itself",
"ey/em": "ey/em/eir/eirs/eirself",
}
def setup(bot):
bot.config.define_section('pronouns', PronounsSection)

# Copied from pronoun.is, leaving a *lot* out.
# If ambiguous, the earlier one will be used.
# This basic set is hard-coded to guarantee that the ten most(ish) common sets
# will work, even if fetching the current pronoun.is set from GitHub fails.
bot.memory['pronoun_sets'] = {
'ze/hir': 'ze/hir/hir/hirs/hirself',
'ze/zir': 'ze/zir/zir/zirs/zirself',
'they/.../themselves': 'they/them/their/theirs/themselves',
'they/.../themself': 'they/them/their/theirs/themself',
'she/her': 'she/her/her/hers/herself',
'he/him': 'he/him/his/his/himself',
'xey/xem': 'xey/xem/xyr/xyrs/xemself',
'sie/hir': 'sie/hir/hir/hirs/hirself',
'it/it': 'it/it/its/its/itself',
'ey/em': 'ey/em/eir/eirs/eirself',
}

if not bot.config.pronouns.fetch_complete_list:
return

# and now try to get the current one
# who needs an API that might never exist?
# (https://github.com/witch-house/pronoun.is/pull/96)
try:
r = requests.get(
'https://github.com/witch-house/pronoun.is/raw/master/resources/pronouns.tab')
r.raise_for_status()
fetched_pairs = _process_pronoun_sets(r.text.splitlines())
except requests.exceptions.RequestException:
# don't do anything, just log the failure and use the hard-coded set
LOGGER.exception("Couldn't fetch full pronouns list; using default set.")
return
except Exception:
# don't care what failed, honestly, since we aren't trying to fix it
LOGGER.exception("Couldn't parse fetched pronouns; using default set.")
return
else:
bot.memory['pronoun_sets'] = dict(fetched_pairs)


def _process_pronoun_sets(set_list):
trie = PronounTrie()
trie.insert_list(set_list)
yield from trie.get_pairs()


class PronounTrieNode:
def __init__(self, source=''):
self.children = {}
"""Child nodes are stored here."""

self.freq = 0
"""Store how many times this node is visited during insertion."""

self.source = source
"""The full pronoun set that caused this node's creation."""


class PronounTrie:
def __init__(self):
self.root = PronounTrieNode()
"""A Trie needs a root entry."""

def insert(self, pronoun_set):
"""Insert a single pronoun set."""
pronoun_set = pronoun_set.replace('\t', '/')
current_node = self.root
for pronoun in pronoun_set.split('/'):
# create a new node if the path doesn't exist
# and use it as the current node
current_node = current_node.children.setdefault(pronoun, PronounTrieNode(pronoun_set))

# increment frequency
current_node.freq += 1

def insert_list(self, set_list):
"""Load a list of pronoun sets all at once."""
for item in set_list:
self.insert(item)

def get_pairs(self, root=None, prefix=''):
"""Yield tuples of ``(prefix, full/pronoun/set)``."""
if root is None:
root = self.root

if root.freq == 1:
yield prefix, root.source
else:
if prefix:
prefix += '/'
for word, node in root.children.items():
yield from self.get_pairs(node, prefix + word)


@plugin.command('pronouns')
Expand Down Expand Up @@ -57,7 +161,7 @@ def pronouns(bot, trigger):


def say_pronouns(bot, nick, pronouns):
for short, set_ in KNOWN_SETS.items():
for short, set_ in bot.memory['pronoun_sets'].items():
if pronouns == set_:
break
short = pronouns
Expand All @@ -72,36 +176,36 @@ def say_pronouns(bot, nick, pronouns):
@plugin.example('.setpronouns they/them')
def set_pronouns(bot, trigger):
"""Set your pronouns."""
pronouns = trigger.group(2)
if not pronouns:
requested_pronouns = trigger.group(2)
if not requested_pronouns:
bot.reply('What pronouns do you use?')
return

disambig = ''
requested_pronoun_split = pronouns.split("/")
if len(requested_pronoun_split) < 5:
requested_pronouns_split = requested_pronouns.split("/")
if len(requested_pronouns_split) < 5:
matching = []
for known_pronoun_set in KNOWN_SETS.values():
known_pronoun_split = known_pronoun_set.split("/")
if known_pronoun_set.startswith(pronouns + "/") or (
len(requested_pronoun_split) == 3
for known_pronoun_set in bot.memory['pronoun_sets'].values():
known_split_set = known_pronoun_set.split("/")
if known_pronoun_set.startswith(requested_pronouns + "/") or (
len(requested_pronouns_split) == 3
and (
(
# "they/.../themself"
requested_pronoun_split[1] == "..."
and requested_pronoun_split[0] == known_pronoun_split[0]
and requested_pronoun_split[2] == known_pronoun_split[4]
requested_pronouns_split[1] == "..."
and requested_pronouns_split[0] == known_split_set[0]
and requested_pronouns_split[2] == known_split_set[4]
)
or (
# "they/them/theirs"
requested_pronoun_split[0:2] == known_pronoun_split[0:2]
and requested_pronoun_split[2] == known_pronoun_split[3]
requested_pronouns_split[:2] == known_split_set[:2]
and requested_pronouns_split[2] == known_split_set[3]
)
)
):
matching.append(known_pronoun_set)

if len(matching) == 0:
if not matching:
bot.reply(
"I'm sorry, I don't know those pronouns. "
"You can give me a set I don't know by formatting it "
Expand All @@ -110,13 +214,13 @@ def set_pronouns(bot, trigger):
)
return

pronouns = matching[0]
if len(matching) > 1:
requested_pronouns = matching.pop(0)
if matching:
disambig = " Or, if you meant one of these, please tell me: {}".format(
", ".join(matching[1:])
", ".join(matching)
)

bot.db.set_nick_value(trigger.nick, 'pronouns', pronouns)
bot.db.set_nick_value(trigger.nick, 'pronouns', requested_pronouns)
bot.reply(
"Thanks for telling me! I'll remember you use {}.{}".format(pronouns, disambig)
"Thanks for telling me! I'll remember you use {}.{}".format(requested_pronouns, disambig)
)