Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tools: add optional arguments to deprecated, and deprecate Ddict #1613

Merged
merged 2 commits into from
Jul 24, 2019
Merged
Show file tree
Hide file tree
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
14 changes: 8 additions & 6 deletions sopel/modules/meetbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@

https://sopel.chat
"""
from __future__ import unicode_literals, absolute_import, print_function, division
from __future__ import absolute_import, division, print_function, unicode_literals

import codecs
import collections
import os
import re
from string import punctuation, whitespace
import time
from string import punctuation, whitespace

from sopel.config.types import StaticSection, FilenameAttribute, ValidatedAttribute
from sopel import formatting, module, tools, web
from sopel.config.types import (FilenameAttribute, StaticSection,
ValidatedAttribute)
from sopel.modules.url import find_title


Expand Down Expand Up @@ -59,7 +61,7 @@ def setup(bot):
bot.config.define_section("meetbot", MeetbotSection)


meetings_dict = tools.Ddict(dict) # Saves metadata about currently running meetings
meetings_dict = collections.defaultdict(dict) # Saves metadata about currently running meetings
"""
meetings_dict is a 2D dict.

Expand Down Expand Up @@ -225,7 +227,7 @@ def startmeeting(bot, trigger):
bot.say(
"Meeting not started: Couldn't create log directory for this channel"
)
meetings_dict[trigger.sender] = tools.Ddict(dict)
meetings_dict[trigger.sender] = collections.defaultdict(dict)
raise
# Okay, meeting started!
log_plain("Meeting started by " + trigger.nick.lower(), trigger.sender)
Expand Down Expand Up @@ -306,7 +308,7 @@ def endmeeting(bot, trigger):
trigger.sender,
)
bot.say("Meeting minutes: " + htmllog_url)
meetings_dict[trigger.sender] = tools.Ddict(dict)
meetings_dict[trigger.sender] = collections.defaultdict(dict)
del meeting_actions[trigger.sender]


Expand Down
122 changes: 108 additions & 14 deletions sopel/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,27 +227,121 @@ def get_sendable_message(text, max_length=400):
return text, excess.lstrip()


def deprecated(old):
def deprecated(reason=None, version=None, removed_in=None, func=None):
"""Decorator to mark deprecated functions in Sopel's API

Any time a decorated function is used, a deprecation warning will be printed
to the console/log-file.
:param str reason: optional text added to the deprecation warning
:param str version: optional version number when the decorated function
is deprecated
:param str removed_in: optional version number when the deprecated function
will be removed
:param callable func: deprecated function
:return: a callable that depends on how the decorator is called; either
the decorated function, or a decorator with the appropriate
parameters

Any time the decorated ``func`` is called, a deprecation warning will be
printed to ``sys.stderr``, with the last frame of the traceback.

It can be used with or without arguments::

from sopel import tools

@tools.deprecated
def func1():
print('func 1')

@tools.deprecated()
def func2():
print('func 2')

@tools.deprecated(reason='obsolete', version='7.0', removed_in='8.0')
def func3():
print('func 3')

which will output the following in a console::

>>> func1()
Deprecated: func1
File "<stdin>", line 1, in <module>
func 1
>>> func2()
Deprecated: func2
File "<stdin>", line 1, in <module>
func 2
>>> func3()
Deprecated since 7.0, will be removed in 8.0: obsolete
File "<stdin>", line 1, in <module>
func 3

.. note::

There is nothing that prevents this decorator to be used on a class's
method, or on any existing callable.

.. versionadded:: 7.0
Parameters ``reason``, ``version``, and ``removed_in``.
"""
@functools.wraps(old)
def new(*args, **kwargs):
print('Function "%s" is deprecated.' % old.__name__, file=sys.stderr)
if not any([reason, version, removed_in, func]):
# common usage: @deprecated()
return deprecated

if callable(reason):
# common usage: @deprecated
return deprecated(func=reason)

if func is None:
# common usage: @deprecated(message, version, removed_in)
def decorator(func):
return deprecated(reason, version, removed_in, func)
return decorator

# now, we have everything we need to have:
# - message is not a callable (could be None)
# - func is not None
# - version and removed_in can be None but that's OK
# so now we can return the actual decorated function

message = reason or getattr(func, '__name__', '<anonymous-function>')

template = 'Deprecated: {message}'
if version and removed_in:
template = (
'Deprecated since {version}, '
'will be removed in {removed_in}: '
'{message}')
elif version:
template = 'Deprecated since {version}: {message}'
elif removed_in:
template = 'Deprecated, will be removed in {removed_in}: {message}'

text = template.format(
message=message, version=version, removed_in=removed_in)

@functools.wraps(func)
def deprecated_func(*args, **kwargs):
stderr(text)
# Only display the last frame
trace = traceback.extract_stack()
stderr(traceback.format_list(trace[:-1])[-1][:-1]) # Only display the last frame
return old(*args, **kwargs)
return new
stderr(traceback.format_list(trace[:-1])[-1][:-1])
return func(*args, **kwargs)

return deprecated_func

# This class was taken from the page below, which no longer exists. The current
# site has nothing related to it, and it was never captured by archive.org.
# Maybe the original author can provide a current link (asked on Twitter)
# http://parand.com/say/index.php/2007/07/13/simple-multi-dimensional-dictionaries-in-python/

# This class was useful before Python 2.5, when ``defaultdict`` was added
# to the built-in ``collections`` module.
# It is now deprecated.
class Ddict(dict):
"""A simple class to make multi-dimensional ``dict``\\s easy to use."""
"""A default dict implementation available for Python 2.x support.

It was used to make multi-dimensional ``dict``\\s easy to use when the
bot worked with Python version < 2.5.

.. deprecated:: 7.0
Use :class:`collections.defaultdict` instead.
"""
@deprecated('use "collections.defaultdict" instead', '7.0', '8.0')
def __init__(self, default=None):
self.default = default

Expand Down