From e765588089370d0e4088fb5e2e3ff3090f84a0e5 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Tue, 1 Sep 2026 16:50:58 +0200 Subject: [PATCH] Memory usage - small improvement by tidying up module imports --- changedetectionio/conditions/default_plugin.py | 2 +- changedetectionio/forms.py | 4 ++-- changedetectionio/html_tools.py | 8 +++----- .../jinja2_custom/extensions/TimeExtension.py | 3 ++- changedetectionio/notification/handler.py | 8 +++++--- changedetectionio/time_handler.py | 3 ++- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/changedetectionio/conditions/default_plugin.py b/changedetectionio/conditions/default_plugin.py index ffc1f7296..3dc456952 100644 --- a/changedetectionio/conditions/default_plugin.py +++ b/changedetectionio/conditions/default_plugin.py @@ -1,7 +1,6 @@ import re import pluggy -from price_parser import Price from loguru import logger from flask_babel import lazy_gettext as _l @@ -70,6 +69,7 @@ def register_field_choices(): @hookimpl def add_data(current_watch_uuid, application_datastruct, ephemeral_data): + from price_parser import Price res = {} if 'text' in ephemeral_data: res['page_filtered_text'] = ephemeral_data['text'] diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index 109fe0bfe..facb8de4b 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -696,14 +696,14 @@ class ValidateCSSJSONXPATHInput(object): raise ValidationError("XPath not permitted in this field!") from lxml import etree, html import elementpath - from changedetectionio.html_tools import SafeXPath3Parser, lxml_guard, lxml_html_parser + from changedetectionio.html_tools import get_safe_xpath3_parser, lxml_guard, lxml_html_parser line = line.replace('xpath:', '') try: # Runs on a Flask request thread - must share the worker's lxml lock. with lxml_guard(): tree = html.fromstring("", parser=lxml_html_parser()) - elementpath.select(tree, line.strip(), parser=SafeXPath3Parser) + elementpath.select(tree, line.strip(), parser=get_safe_xpath3_parser()) except elementpath.ElementPathError as e: message = field.gettext('\'%(expression)s\' is not a valid XPath expression. (%(error)s)') raise ValidationError(message % {'expression': line, 'error': str(e)}) diff --git a/changedetectionio/html_tools.py b/changedetectionio/html_tools.py index ae869c04d..8d4504e3d 100644 --- a/changedetectionio/html_tools.py +++ b/changedetectionio/html_tools.py @@ -164,7 +164,8 @@ _DEFAULT_UNSAFE_XPATH3_FUNCTIONS = [ ] -def _build_safe_xpath3_parser(): +@lru_cache(maxsize=1) +def get_safe_xpath3_parser(): """Return an XPath3Parser subclass with filesystem/environment access functions removed. XPath 3.0 includes functions that can read arbitrary files or environment variables: @@ -196,9 +197,6 @@ def _build_safe_xpath3_parser(): return SafeXPath3Parser -# Module-level singleton — built once, reused everywhere. -SafeXPath3Parser = _build_safe_xpath3_parser() - # Doesn't look like python supports forward slash auto enclosure in re.findall # So convert it to inline flag "(?i)foobar" type configuration @lru_cache(maxsize=100) @@ -386,7 +384,7 @@ def xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False # This allows //title to match elements in the default namespace namespaces[''] = tree.nsmap[None] - r = elementpath.select(tree, xpath_filter.strip(), namespaces=namespaces, parser=SafeXPath3Parser) + r = elementpath.select(tree, xpath_filter.strip(), namespaces=namespaces, parser=get_safe_xpath3_parser()) #@note: //title/text() now works with default namespaces (fixed by registering '' prefix) #@note: //title/text() wont work where CDATA.. (use cdata_in_document_to_text first) diff --git a/changedetectionio/jinja2_custom/extensions/TimeExtension.py b/changedetectionio/jinja2_custom/extensions/TimeExtension.py index 9becdda7b..15e8f5c7f 100644 --- a/changedetectionio/jinja2_custom/extensions/TimeExtension.py +++ b/changedetectionio/jinja2_custom/extensions/TimeExtension.py @@ -80,7 +80,6 @@ Note: This extension uses the Arrow library for timezone-aware datetime handling. All timezone names should be valid IANA timezone identifiers (e.g., 'America/New_York'). """ -import arrow from jinja2 import nodes from jinja2.ext import Extension @@ -125,6 +124,7 @@ class TimeExtension(Extension): _datetime('UTC', '+', 'hours=2,minutes=30', '%Y-%m-%d %H:%M:%S') # Returns current time + 2.5 hours """ + import arrow # Use default timezone if none specified if not timezone or timezone == '': timezone = self.environment.default_timezone @@ -162,6 +162,7 @@ class TimeExtension(Extension): _now('America/New_York', '%Y-%m-%d %H:%M:%S') # Returns current time in New York timezone """ + import arrow # Use default timezone if none specified if not timezone or timezone == '': timezone = self.environment.default_timezone diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py index 6329bdc58..cca332873 100644 --- a/changedetectionio/notification/handler.py +++ b/changedetectionio/notification/handler.py @@ -1,11 +1,8 @@ import time import re -import apprise -from apprise import NotifyFormat from loguru import logger from urllib.parse import urlparse -from .apprise_plugin.assets import apprise_asset, APPRISE_AVATAR_URL from .email_helpers import as_monospaced_html_email from ..diff import HTML_REMOVED_STYLE, REMOVED_PLACEMARKER_OPEN, REMOVED_PLACEMARKER_CLOSED, ADDED_PLACEMARKER_OPEN, HTML_ADDED_STYLE, \ ADDED_PLACEMARKER_CLOSED, CHANGED_INTO_PLACEMARKER_OPEN, CHANGED_INTO_PLACEMARKER_CLOSED, CHANGED_PLACEMARKER_OPEN, \ @@ -64,6 +61,7 @@ def notification_format_align_with_apprise(n_format : str): :param n_format: :return: """ + from apprise import NotifyFormat if not n_format: return NotifyFormat.TEXT.value @@ -208,6 +206,7 @@ def replace_placemarkers_in_text(text, url, requested_output_format): def apply_service_tweaks(url, n_body, n_title, requested_output_format): + from .apprise_plugin.assets import APPRISE_AVATAR_URL logger.debug(f"Applying markup in '{requested_output_format}' mode") # Re 323 - Limit discord length to their 2000 char limit total or it wont send. @@ -305,6 +304,9 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): def process_notification(n_object: NotificationContextData, datastore): + import apprise + from apprise import NotifyFormat + from .apprise_plugin.assets import apprise_asset from changedetectionio.jinja2_custom import render as jinja_render from . import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_format, valid_notification_formats # be sure its registered diff --git a/changedetectionio/time_handler.py b/changedetectionio/time_handler.py index 9def380a2..0081c48f0 100644 --- a/changedetectionio/time_handler.py +++ b/changedetectionio/time_handler.py @@ -1,6 +1,5 @@ from functools import lru_cache -import arrow from enum import IntEnum @@ -32,6 +31,7 @@ def am_i_inside_time( Returns: bool: True if the current time is within the time range, False otherwise. """ + import arrow # Parse the target day of the week try: target_weekday = Weekday[day_of_week.capitalize()] @@ -91,6 +91,7 @@ def is_within_schedule(time_schedule_limit, default_tz="UTC"): Returns: bool: True if current time is within the schedule, False otherwise. """ + import arrow if time_schedule_limit and time_schedule_limit.get('enabled'): # Get the timezone the time schedule is in, so we know what day it is there tz_name = time_schedule_limit.get('timezone')