devx: add deprecation dumpster (#27887)

* feat: Add deprecation_dumpster.py file

* docs: add jovial and jocose docstring for frappe/deprecation_dumpster.py

* refactor: fill the dumpster with its own kind

* refactor: move to the deprecation dumpster

* chore: color coding class

* fix: only check import error when import errors
This commit is contained in:
David Arnold 2024-10-08 18:56:10 +02:00 committed by GitHub
parent e7776021aa
commit 8cfeb156df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 361 additions and 133 deletions

View file

@ -326,19 +326,23 @@ def connect(site: str | None = None, db_name: str | None = None, set_admin_as_us
from frappe.database import get_db
if site:
from frappe.utils.deprecations import deprecation_warning
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
"unknown",
"v17",
"Calling frappe.connect with the site argument is deprecated and will be removed in next major version. "
"Instead, explicitly invoke frappe.init(site) prior to calling frappe.connect(), if initializing the site is necessary."
"Instead, explicitly invoke frappe.init(site) prior to calling frappe.connect(), if initializing the site is necessary.",
)
init(site)
if db_name:
from frappe.utils.deprecations import deprecation_warning
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
"unknown",
"v17",
"Calling frappe.connect with the db_name argument is deprecated and will be removed in next major version. "
"Instead, explicitly invoke frappe.init(site) with the right config prior to calling frappe.connect(), if necessary."
"Instead, explicitly invoke frappe.init(site) with the right config prior to calling frappe.connect(), if necessary.",
)
assert db_name or local.conf.db_user, "site must be fully initialized, db_user missing"
@ -1210,9 +1214,11 @@ def generate_hash(txt: str | None = None, length: int = 56) -> str:
import secrets
if txt:
from frappe.utils.deprecations import deprecation_warning
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning("The `txt` parameter is deprecated and will be removed in a future release.")
deprecation_warning(
"unknown", "v17", "The `txt` parameter is deprecated and will be removed in a future release."
)
return secrets.token_hex(math.ceil(length / 2))[:length]

View file

@ -27,7 +27,6 @@ from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest, val
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import CallbackManager, cint, get_site_name
from frappe.utils.data import escape_html
from frappe.utils.deprecations import deprecation_warning
from frappe.utils.error import log_error_snapshot
from frappe.website.serve import get_response
@ -105,8 +104,12 @@ def application(request: Request):
response = Response()
elif frappe.form_dict.cmd:
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
f"{frappe.form_dict.cmd}: Sending `cmd` for RPC calls is deprecated, call REST API instead `/api/method/cmd`"
"unknown",
"v17",
f"{frappe.form_dict.cmd}: Sending `cmd` for RPC calls is deprecated, call REST API instead `/api/method/cmd`",
)
frappe.handler.handle()
response = frappe.utils.response.build_response("json")

View file

@ -22,7 +22,6 @@ from frappe.twofactor import (
should_run_2fa,
)
from frappe.utils import cint, date_diff, datetime, get_datetime, today
from frappe.utils.deprecations import deprecation_warning
from frappe.utils.password import check_password, get_decrypted_password
from frappe.website.utils import get_home_page
@ -518,7 +517,9 @@ class LoginAttemptTracker:
:param lock_interval: Locking interval incase of maximum failed attempts
"""
if user_name:
deprecation_warning("`username` parameter is deprecated, use `key` instead.")
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning("unknown", "v17", "`username` parameter is deprecated, use `key` instead.")
self.key = key or user_name
self.lock_interval = datetime.timedelta(seconds=lock_interval)
self.max_failed_logins = max_consecutive_login_attempts

View file

@ -12,7 +12,6 @@ from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission
from frappe.model.utils import is_virtual_doctype
from frappe.utils import get_safe_filters
from frappe.utils.deprecations import deprecated
if TYPE_CHECKING:
from frappe.model.document import Document
@ -328,28 +327,9 @@ def get_password(doctype, name, fieldname):
return frappe.get_doc(doctype, name).get_password(fieldname)
@frappe.whitelist()
@deprecated
def get_js(items):
"""Load JS code files. Will also append translations
and extend `frappe._messages`
from frappe.deprecation_dumpster import get_js as _get_js
:param items: JSON list of paths of the js files to be loaded."""
items = json.loads(items)
out = []
for src in items:
src = src.strip("/").split("/")
if ".." in src or src[0] != "assets":
frappe.throw(_("Invalid file path: {0}").format("/".join(src)))
contentpath = os.path.join(frappe.local.sites_path, *src)
with open(contentpath) as srcfile:
code = frappe.utils.cstr(srcfile.read())
out.append(code)
return out
get_js = frappe.whitelist()(_get_js)
@frappe.whitelist(allow_guest=True)

View file

@ -32,7 +32,6 @@ from frappe.utils import (
today,
)
from frappe.utils.data import sha256_hash
from frappe.utils.deprecations import deprecated, deprecation_warning
from frappe.utils.password import check_password, get_password_reset_limit
from frappe.utils.password import update_password as _update_password
from frappe.utils.user import get_system_managers
@ -227,9 +226,7 @@ class User(Document):
self.roles = [r for r in self.roles if r.role in new_roles]
self.append_roles(*new_roles)
@deprecated
def validate_roles(self):
self.populate_role_profile_roles()
from frappe.deprecation_dumpster import validate_roles
def move_role_profile_name_to_role_profiles(self):
"""This handles old role_profile_name field if programatically set.
@ -243,8 +240,12 @@ class User(Document):
self.role_profile_name = None
return
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
"The field `role_profile_name` is deprecated and will be removed in v16, use `role_profiles` child table instead."
"unknown",
"v16",
"The field `role_profile_name` is deprecated and will be removed in v16, use `role_profiles` child table instead.",
)
self.append("role_profiles", {"role_profile": self.role_profile_name})
self.role_profile_name = None
@ -908,12 +909,15 @@ def update_password(
@frappe.whitelist(allow_guest=True)
def test_password_strength(new_password: str, key=None, old_password=None, user_data: tuple | None = None):
from frappe.utils.deprecations import deprecation_warning
from frappe.utils.password_strength import test_password_strength as _test_password_strength
if key is not None or old_password is not None:
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
"Arguments `key` and `old_password` are deprecated in function `test_password_strength`."
"unknown",
"v17",
"Arguments `key` and `old_password` are deprecated in function `test_password_strength`.",
)
enable_password_policy = frappe.get_system_settings("enable_password_policy")

View file

@ -32,7 +32,6 @@ from frappe.monitor import get_trace_id
from frappe.query_builder.functions import Count
from frappe.utils import CallbackManager, cint, get_datetime, get_table_name, getdate, now, sbool
from frappe.utils import cast as cast_fieldtype
from frappe.utils.deprecations import deprecated, deprecation_warning
if TYPE_CHECKING:
from psycopg2 import connection as PostgresConnection
@ -962,8 +961,12 @@ class Database:
if dn is None or dt == dn:
if not is_single_doctype(dt):
return
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
"Calling db.set_value on single doctype is deprecated. This behaviour will be removed in future. Use db.set_single_value instead."
"unknown",
"v17",
"Calling db.set_value on single doctype is deprecated. This behaviour will be removed in future. Use db.set_single_value instead.",
)
self.set_single_value(
doctype=dt,
@ -1243,10 +1246,9 @@ class Database:
# implemented in specific class
raise NotImplementedError
@staticmethod
@deprecated
def is_column_missing(e):
return frappe.db.is_missing_column(e)
from frappe.deprecation_dumpster import is_column_missing as _is_column_missing
is_column_missing = staticmethod(_is_column_missing)
def get_descendants(self, doctype, name):
"""Return descendants of the group node in tree"""

View file

@ -0,0 +1,290 @@
"""
Welcome to the Deprecation Dumpster: Where Old Code Goes to Party! 🎉🗑
This file is the final resting place (or should we say, "retirement home"?) for all the deprecated functions and methods of the Frappe framework. It's like a code nursing home, but with more monkey-patching and less bingo.
Each function or method that checks in here comes with its own personalized decorator, complete with:
1. The date it was marked for deprecation (its "over the hill" birthday)
2. The Frappe version in which it will be removed (its "graduation" to the great codebase in the sky)
3. A user-facing note on alternative solutions (its "parting wisdom")
Warning: The global namespace herein is more patched up than a sailor's favorite pair of jeans. Proceed with caution and a sense of humor!
Remember, deprecated doesn't mean useless - it just means these functions are enjoying their golden years before their final bow. Treat them with respect, and maybe bring them some virtual prune juice.
Enjoy your stay in the Deprecation Dumpster, where every function gets a second chance to shine (or at least, to not break everything).
"""
import inspect
import os
import sys
import warnings
def colorize(text, color_code):
if sys.stdout.isatty():
return f"\033[{color_code}m{text}\033[0m"
return text
class Color:
RED = 91
YELLOW = 93
CYAN = 96
try:
# since python 3.13, PEP 702
from warnings import deprecated as _deprecated
except ImportError:
import functools
import warnings
from collections.abc import Callable
from typing import Optional, TypeVar, Union, overload
T = TypeVar("T", bound=Callable)
def _deprecated(message: str, category=DeprecationWarning, stacklevel=1) -> Callable[[T], T]:
def decorator(func: T) -> T:
@functools.wraps(func)
def wrapper(*args, **kwargs):
if message:
warning_msg = f"{func.__name__} is deprecated.\n{message}"
else:
warning_msg = f"{func.__name__} is deprecated."
warnings.warn(warning_msg, category=category, stacklevel=stacklevel + 1)
return func(*args, **kwargs)
return wrapper
wrapper.__deprecated__ = True # hint for the type checker
return decorator
def deprecated(original: str, marked: str, graduation: str, msg: str, stacklevel: int = 1):
"""Decorator to wrap a function/method as deprecated.
Arguments:
- original: frappe.utils.make_esc (fully qualified)
- marked: 2024-09-13 (the date it has been marked)
- graduation: v17 (generally: current version + 2)
"""
def decorator(func):
# Get the filename of the caller
frame = inspect.currentframe()
caller_filepath = frame.f_back.f_code.co_filename
if os.path.basename(caller_filepath) != "deprecation_dumpster.py":
raise RuntimeError(
colorize("The deprecated function ", Color.YELLOW)
+ colorize(func.__name__, Color.CYAN)
+ colorize(" can only be called from ", Color.YELLOW)
+ colorize("frappe/deprecation_dumpster.py\n", Color.CYAN)
+ colorize("Move the entire function there and import it back via adding\n ", Color.YELLOW)
+ colorize(f"from frappe.deprecation_dumpster import {func.__name__}\n", Color.CYAN)
+ colorize("to file\n ", Color.YELLOW)
+ colorize(caller_filepath, Color.CYAN)
)
return functools.wraps(func)(
_deprecated(
colorize(f"`{original}`", Color.CYAN)
+ colorize(
f" was moved (DATE: {marked}) to frappe/deprecation_dumpster.py"
f" for removal (from {graduation} onwards); note:\n ",
Color.RED,
)
+ colorize(f"{msg}\n", Color.YELLOW),
stacklevel=stacklevel,
)
)(func)
return decorator
def deprecation_warning(marked: str, graduation: str, msg: str):
"""Warn in-place from a deprecated code path, for objects use `@deprecated` decorator from the deprectation_dumpster"
Arguments:
- marked: 2024-09-13 (the date it has been marked)
- graduation: v17 (generally: current version + 2)
"""
warnings.warn(
colorize(
f"This codepath was marked (DATE: {marked}) deprecated"
f" for removal (from {graduation} onwards); note:\n ",
Color.RED,
)
+ colorize(f"{msg}\n", Color.YELLOW),
category=DeprecationWarning,
stacklevel=2,
)
### Party starts here
def _old_deprecated(func):
return deprecated(
"frappe.deprecations.deprecated",
"2024-09-13",
"v17",
"Make use of the frappe/deprecation_dumpster.py file, instead. 🎉🗑️",
)(_deprecated("")(func))
def _old_deprecation_warning(msg):
@deprecated(
"frappe.deprecations.deprecation_warning",
"2024-09-13",
"v17",
"Use frappe.deprecation_dumpster.deprecation_warning, instead. 🎉🗑️",
)
def deprecation_warning(message, category=DeprecationWarning, stacklevel=1):
warnings.warn(message=message, category=category, stacklevel=stacklevel + 2)
return deprecation_warning(msg)
@deprecated("frappe.utils.make_esc", "unknown", "v17", "Not used anymore.")
def make_esc(esc_chars):
"""
Function generator for Escaping special characters
"""
return lambda s: "".join("\\" + c if c in esc_chars else c for c in s)
@deprecated(
"frappe.db.is_column_missing",
"unknown",
"v17",
"Renamed to frappe.db.is_missing_column.",
)
def is_column_missing(e):
import frappe
return frappe.db.is_missing_column(e)
@deprecated(
"frappe.desk.doctype.bulk_update.bulk_update",
"unknown",
"v17",
"Unknown.",
)
def show_progress(docnames, message, i, description):
import frappe
n = len(docnames)
frappe.publish_progress(float(i) * 100 / n, title=message, description=description)
@deprecated(
"frappe.client.get_js",
"unknown",
"v17",
"Unknown.",
)
def get_js(items):
"""Load JS code files. Will also append translations
and extend `frappe._messages`
:param items: JSON list of paths of the js files to be loaded."""
import json
import frappe
from frappe import _
items = json.loads(items)
out = []
for src in items:
src = src.strip("/").split("/")
if ".." in src or src[0] != "assets":
frappe.throw(_("Invalid file path: {0}").format("/".join(src)))
contentpath = os.path.join(frappe.local.sites_path, *src)
with open(contentpath) as srcfile:
code = frappe.utils.cstr(srcfile.read())
out.append(code)
return out
@deprecated(
"frappe.utils.print_format.read_multi_pdf",
"unknown",
"v17",
"Unknown.",
)
def read_multi_pdf(output) -> bytes:
from io import BytesIO
with BytesIO() as merged_pdf:
output.write(merged_pdf)
return merged_pdf.getvalue()
@deprecated("frappe.gzip_compress", "unknown", "v17", "Use py3 methods directly (this was compat for py2).")
def gzip_compress(data, compresslevel=9):
"""Compress data in one shot and return the compressed string.
Optional argument is the compression level, in range of 0-9.
"""
import io
from gzip import GzipFile
buf = io.BytesIO()
with GzipFile(fileobj=buf, mode="wb", compresslevel=compresslevel) as f:
f.write(data)
return buf.getvalue()
@deprecated("frappe.gzip_decompress", "unknown", "v17", "Use py3 methods directly (this was compat for py2).")
def gzip_decompress(data):
"""Decompress a gzip compressed string in one shot.
Return the decompressed string.
"""
import io
from gzip import GzipFile
with GzipFile(fileobj=io.BytesIO(data)) as f:
return f.read()
@deprecated(
"frappe.email.doctype.email_queue.email_queue.send_mail",
"unknown",
"v17",
"Unknown.",
)
def send_mail(email_queue_name, smtp_server_instance=None):
"""This is equivalent to EmailQueue.send.
This provides a way to make sending mail as a background job.
"""
from frappe.email.doctype.email_queue.email_queue import EmailQueue
record = EmailQueue.find(email_queue_name)
record.send(smtp_server_instance=smtp_server_instance)
@deprecated(
"frappe.geo.country_info.get_translated_dict",
"unknown",
"v17",
"Use frappe.geo.country_info.get_translated_countries, instead.",
)
def get_translated_dict():
from frappe.geo.country_info import get_translated_countries
return get_translated_countries()
@deprecated(
"User.validate_roles",
"unknown",
"v17",
"Use User.populate_role_profile_roles, instead.",
)
def validate_roles(self):
self.populate_role_profile_roles()

View file

@ -6,7 +6,6 @@ from frappe import _
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils.deprecations import deprecated
from frappe.utils.scheduler import is_scheduler_inactive
@ -111,7 +110,4 @@ def _bulk_action(doctype, docnames, action, data, task_id=None):
return failed
@deprecated
def show_progress(docnames, message, i, description):
n = len(docnames)
frappe.publish_progress(float(i) * 100 / n, title=message, description=description)
from frappe.deprecation_dumpster import show_progress

View file

@ -32,7 +32,6 @@ from frappe.utils import (
sbool,
split_emails,
)
from frappe.utils.deprecations import deprecated
from frappe.utils.verified_command import get_signed_params
@ -223,15 +222,9 @@ class EmailQueue(Document):
).run()
@task(queue="short")
@deprecated
def send_mail(email_queue_name, smtp_server_instance: SMTPServer = None):
"""This is equivalent to EmailQueue.send.
from frappe.deprecation_dumpster import send_mail as _send_mail
This provides a way to make sending mail as a background job.
"""
record = EmailQueue.find(email_queue_name)
record.send(smtp_server_instance=smtp_server_instance)
send_mail = task(queue="short")(_send_mail)
class SendMailContext:

View file

@ -8,7 +8,6 @@ import os
from functools import lru_cache
import frappe
from frappe.utils.deprecations import deprecated
from frappe.utils.momentjs import get_all_timezones
@ -39,9 +38,7 @@ def _get_country_timezone_info():
return {"country_info": get_all(), "all_timezones": get_all_timezones()}
@deprecated
def get_translated_dict():
return get_translated_countries()
from frappe.deprecation_dumpster import get_translated_dict
def get_translated_countries():

View file

@ -15,7 +15,7 @@ from frappe.core.doctype.server_script.server_script_utils import get_server_scr
from frappe.monitor import add_data_to_monitor
from frappe.utils import cint
from frappe.utils.csvutils import build_csv_response
from frappe.utils.deprecations import deprecated, deprecation_warning
from frappe.utils.deprecations import deprecated
from frappe.utils.image import optimize_image
from frappe.utils.response import build_response
@ -241,8 +241,12 @@ def get_attr(cmd):
if "." in cmd:
method = frappe.get_attr(cmd)
else:
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
f"Calling shorthand for {cmd} is deprecated, please specify full path in RPC call."
"unknown",
"v17",
f"Calling shorthand for {cmd} is deprecated, please specify full path in RPC call.",
)
method = globals()[cmd]
return method

View file

@ -23,9 +23,10 @@ from typing import TypedDict
from werkzeug.test import Client
from frappe.deprecation_dumpster import gzip_compress, gzip_decompress, make_esc
# utility functions like cint, int, flt, etc.
from frappe.utils.data import *
from frappe.utils.deprecations import deprecated
from frappe.utils.html_utils import sanitize_html
EMAIL_NAME_PATTERN = re.compile(r"[^A-Za-z0-9\u00C0-\u024F\/\_\' ]+")
@ -423,14 +424,6 @@ def get_file_timestamp(fn):
return None
# to be deprecated
def make_esc(esc_chars):
"""
Function generator for Escaping special characters
"""
return lambda s: "".join("\\" + c if c in esc_chars else c for c in s)
# esc / unescape characters -- used for command line
def esc(s, esc_chars):
"""
@ -888,34 +881,6 @@ def call(fn, *args, **kwargs):
return json.loads(frappe.as_json(frappe.call(fn, *args, **kwargs)))
# Following methods are aken as-is from Python 3 codebase
# since gzip.compress and gzip.decompress are not available in Python 2.7
@deprecated
def gzip_compress(data, compresslevel=9):
"""Compress data in one shot and return the compressed string.
Optional argument is the compression level, in range of 0-9.
"""
from gzip import GzipFile
buf = io.BytesIO()
with GzipFile(fileobj=buf, mode="wb", compresslevel=compresslevel) as f:
f.write(data)
return buf.getvalue()
@deprecated
def gzip_decompress(data):
"""Decompress a gzip compressed string in one shot.
Return the decompressed string.
"""
from gzip import GzipFile
with GzipFile(fileobj=io.BytesIO(data)) as f:
return f.read()
def get_safe_filters(filters):
try:
filters = json.loads(filters)

View file

@ -26,7 +26,6 @@ import frappe.monitor
from frappe import _
from frappe.utils import CallbackManager, cint, get_bench_id
from frappe.utils.commands import log
from frappe.utils.deprecations import deprecation_warning
from frappe.utils.redis_queue import RedisQueue
# TTL to keep RQ job logs in redis for.
@ -119,11 +118,19 @@ def enqueue(
job_id = create_job_id(job_id)
if job_name:
deprecation_warning("Using enqueue with `job_name` is deprecated, use `job_id` instead.")
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
"unknown", "v17", "Using enqueue with `job_name` is deprecated, use `job_id` instead."
)
if not is_async and not frappe.flags.in_test:
from frappe.deprecation_dumpster import deprecation_warning
deprecation_warning(
"Using enqueue with is_async=False outside of tests is not recommended, use now=True instead."
"unknown",
"v17",
"Using enqueue with is_async=False outside of tests is not recommended, use now=True instead.",
)
call_directly = now or (not is_async and not frappe.flags.in_test)

View file

@ -1,27 +1,12 @@
""" Utils for deprecating functionality in Framework.
"""Utils for deprecating functionality in Framework.
WARNING: This file is internal, instead of depending just copy the code or use deprecation
libraries.
"""
import functools
import warnings
def deprecated(func):
"""Decorator to wrap a function/method as deprecated."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
deprecation_warning(
f"{func.__name__} is deprecated and will be removed in next major version.",
stacklevel=1,
)
return func(*args, **kwargs)
return wrapper
def deprecation_warning(message, category=DeprecationWarning, stacklevel=1):
"""like warnings.warn but with auto incremented sane stacklevel."""
warnings.warn(message=message, category=category, stacklevel=stacklevel + 2)
from frappe.deprecation_dumpster import (
_old_deprecated as deprecated,
)
from frappe.deprecation_dumpster import (
_old_deprecation_warning as deprecation_warning,
)

View file

@ -10,7 +10,6 @@ import frappe
from frappe import _
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.translate import print_language
from frappe.utils.deprecations import deprecated
from frappe.utils.pdf import get_pdf
no_cache = 1
@ -214,11 +213,7 @@ def _download_multi_pdf(
frappe.local.response.type = "pdf"
@deprecated
def read_multi_pdf(output: PdfWriter) -> bytes:
with BytesIO() as merged_pdf:
output.write(merged_pdf)
return merged_pdf.getvalue()
from frappe.deprecation_dumpster import read_multi_pdf
@frappe.whitelist(allow_guest=True)