refactor: remove package-level caching to support dynamic installation
Remove package-level caching in cnr_utils and node_package modules to enable proper dynamic custom node installation and version switching without ComfyUI server restarts. Key Changes: - Remove @lru_cache decorators from version-sensitive functions - Remove cached_property from NodePackage for dynamic state updates - Add comprehensive test suite with parallel execution support - Implement version switching tests (CNR ↔ Nightly) - Add case sensitivity integration tests - Improve error handling and logging API Priority Rules (manager_core.py:1801): - Enabled-Priority: Show only enabled version when both exist - CNR-Priority: Show only CNR when both CNR and Nightly are disabled - Prevents duplicate package entries in /v2/customnode/installed API - Cross-match using cnr_id and aux_id for CNR ↔ Nightly detection Test Infrastructure: - 8 test files with 59 comprehensive test cases - Parallel test execution across 5 isolated environments - Automated test scripts with environment setup - Configurable timeout (60 minutes default) - Support for both master and dr-support-pip-cm branches Bug Fixes: - Fix COMFYUI_CUSTOM_NODES_PATH environment variable export - Resolve test fixture regression with module-level variables - Fix import timing issues in test configuration - Register pytest integration marker to eliminate warnings - Fix POSIX compliance in shell scripts (((var++)) → $((var + 1))) Documentation: - CNR_VERSION_MANAGEMENT_DESIGN.md v1.0 → v1.1 with API priority rules - Add test guides and execution documentation (TESTING_PROMPT.md) - Add security-enhanced installation guide - Create CLI migration guides and references - Document package version management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,11 @@ variables = {}
|
||||
APIs = {}
|
||||
|
||||
|
||||
pip_overrides = {}
|
||||
pip_blacklist = {}
|
||||
pip_downgrade_blacklist = {}
|
||||
|
||||
|
||||
def register_api(k, f):
|
||||
global APIs
|
||||
APIs[k] = f
|
||||
|
||||
@@ -12,6 +12,10 @@ from . import manager_util
|
||||
import requests
|
||||
import toml
|
||||
import logging
|
||||
from . import git_utils
|
||||
from cachetools import TTLCache, cached
|
||||
|
||||
query_ttl_cache = TTLCache(maxsize=100, ttl=60)
|
||||
|
||||
base_url = "https://api.comfy.org"
|
||||
|
||||
@@ -20,6 +24,29 @@ lock = asyncio.Lock()
|
||||
|
||||
is_cache_loading = False
|
||||
|
||||
|
||||
def normalize_package_name(name: str) -> str:
|
||||
"""
|
||||
Normalize package name for case-insensitive matching.
|
||||
|
||||
This follows the same normalization pattern used throughout CNR:
|
||||
- Strip leading/trailing whitespace
|
||||
- Convert to lowercase
|
||||
|
||||
Args:
|
||||
name: Package name to normalize (e.g., "ComfyUI_SigmoidOffsetScheduler" or " NodeName ")
|
||||
|
||||
Returns:
|
||||
Normalized package name (e.g., "comfyui_sigmoidoffsetscheduler")
|
||||
|
||||
Examples:
|
||||
>>> normalize_package_name("ComfyUI_SigmoidOffsetScheduler")
|
||||
"comfyui_sigmoidoffsetscheduler"
|
||||
>>> normalize_package_name(" NodeName ")
|
||||
"nodename"
|
||||
"""
|
||||
return name.strip().lower()
|
||||
|
||||
async def get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
try:
|
||||
return await _get_cnr_data(cache_mode, dont_wait)
|
||||
@@ -37,7 +64,6 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
page = 1
|
||||
|
||||
full_nodes = {}
|
||||
|
||||
|
||||
# Determine form factor based on environment and platform
|
||||
is_desktop = bool(os.environ.get('__COMFYUI_DESKTOP_VERSION__'))
|
||||
@@ -138,7 +164,7 @@ def map_node_version(api_node_version):
|
||||
Maps node version data from API response to NodeVersion dataclass.
|
||||
|
||||
Args:
|
||||
api_data (dict): The 'node_version' part of the API response.
|
||||
api_node_version (dict): The 'node_version' part of the API response.
|
||||
|
||||
Returns:
|
||||
NodeVersion: An instance of NodeVersion dataclass populated with data from the API.
|
||||
@@ -189,6 +215,80 @@ def install_node(node_id, version=None):
|
||||
return None
|
||||
|
||||
|
||||
@cached(query_ttl_cache)
|
||||
def get_nodepack(packname):
|
||||
"""
|
||||
Retrieves the nodepack
|
||||
|
||||
Args:
|
||||
packname (str): The unique identifier of the node.
|
||||
|
||||
Returns:
|
||||
nodepack info {id, latest_version}
|
||||
"""
|
||||
url = f"{base_url}/nodes/{packname}"
|
||||
|
||||
response = requests.get(url, verify=not manager_util.bypass_ssl)
|
||||
if response.status_code == 200:
|
||||
info = response.json()
|
||||
|
||||
res = {
|
||||
'id': info['id']
|
||||
}
|
||||
|
||||
if 'latest_version' in info:
|
||||
res['latest_version'] = info['latest_version']['version']
|
||||
|
||||
if 'repository' in info:
|
||||
res['repository'] = info['repository']
|
||||
|
||||
return res
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@cached(query_ttl_cache)
|
||||
def get_nodepack_by_url(url):
|
||||
"""
|
||||
Retrieves the nodepack info for installation.
|
||||
|
||||
Args:
|
||||
url (str): The unique identifier of the node.
|
||||
|
||||
Returns:
|
||||
NodeVersion: Node version data or error message.
|
||||
"""
|
||||
|
||||
# example query: https://api.comfy.org/nodes/search?repository_url_search=ltdrdata/ComfyUI-Impact-Pack&limit=1
|
||||
url = f"nodes/search?repository_url_search={url}&limit=1"
|
||||
|
||||
response = requests.get(url, verify=not manager_util.bypass_ssl)
|
||||
if response.status_code == 200:
|
||||
# Convert the API response to a NodeVersion object
|
||||
info = response.json().get('nodes', [])
|
||||
if len(info) > 0:
|
||||
info = info[0]
|
||||
repo_url = info['repository']
|
||||
|
||||
if git_utils.compact_url(url) != git_utils.compact_url(repo_url):
|
||||
return None
|
||||
|
||||
res = {
|
||||
'id': info['id']
|
||||
}
|
||||
|
||||
if 'latest_version' in info:
|
||||
res['latest_version'] = info['latest_version']['version']
|
||||
|
||||
res['repository'] = info['repository']
|
||||
|
||||
return res
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def all_versions_of_node(node_id):
|
||||
url = f"{base_url}/nodes/{node_id}/versions?statuses=NodeVersionStatusActive&statuses=NodeVersionStatusPending"
|
||||
|
||||
@@ -211,8 +311,7 @@ def read_cnr_info(fullpath):
|
||||
data = toml.load(f)
|
||||
|
||||
project = data.get('project', {})
|
||||
name = project.get('name').strip().lower()
|
||||
original_name = project.get('name')
|
||||
name = project.get('name').strip()
|
||||
|
||||
# normalize version
|
||||
# for example: 2.5 -> 2.5.0
|
||||
@@ -224,7 +323,6 @@ def read_cnr_info(fullpath):
|
||||
if name and version: # repository is optional
|
||||
return {
|
||||
"id": name,
|
||||
"original_name": original_name,
|
||||
"version": version,
|
||||
"url": repository
|
||||
}
|
||||
@@ -254,4 +352,3 @@ def read_cnr_id(fullpath):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -77,6 +77,14 @@ def normalize_to_github_id(url) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def compact_url(url):
|
||||
github_id = normalize_to_github_id(url)
|
||||
if github_id is not None:
|
||||
return github_id
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def get_url_for_clone(url):
|
||||
url = normalize_url(url)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class InstalledNodePackage:
|
||||
fullpath: str
|
||||
disabled: bool
|
||||
version: str
|
||||
repo_url: str = None # Git repository URL for nightly packages
|
||||
|
||||
@property
|
||||
def is_unknown(self) -> bool:
|
||||
@@ -46,6 +47,8 @@ class InstalledNodePackage:
|
||||
|
||||
@staticmethod
|
||||
def from_fullpath(fullpath: str, resolve_from_path) -> InstalledNodePackage:
|
||||
from . import git_utils
|
||||
|
||||
parent_folder_name = os.path.basename(os.path.dirname(fullpath))
|
||||
module_name = os.path.basename(fullpath)
|
||||
|
||||
@@ -54,6 +57,10 @@ class InstalledNodePackage:
|
||||
disabled = True
|
||||
elif parent_folder_name == ".disabled":
|
||||
# Nodes under custom_nodes/.disabled/* are disabled
|
||||
# Parse directory name format: packagename@version
|
||||
# Examples:
|
||||
# comfyui_sigmoidoffsetscheduler@nightly → id: comfyui_sigmoidoffsetscheduler, version: nightly
|
||||
# comfyui_sigmoidoffsetscheduler@1_0_2 → id: comfyui_sigmoidoffsetscheduler, version: 1.0.2
|
||||
node_id = module_name
|
||||
disabled = True
|
||||
else:
|
||||
@@ -61,12 +68,35 @@ class InstalledNodePackage:
|
||||
disabled = False
|
||||
|
||||
info = resolve_from_path(fullpath)
|
||||
repo_url = None
|
||||
version_from_dirname = None
|
||||
|
||||
# For disabled packages, try to extract version from directory name
|
||||
if disabled and parent_folder_name == ".disabled" and '@' in module_name:
|
||||
parts = module_name.split('@')
|
||||
if len(parts) == 2:
|
||||
node_id = parts[0] # Use the normalized name from directory
|
||||
version_from_dirname = parts[1].replace('_', '.') # Convert 1_0_2 → 1.0.2
|
||||
|
||||
if info is None:
|
||||
version = 'unknown'
|
||||
version = version_from_dirname if version_from_dirname else 'unknown'
|
||||
else:
|
||||
node_id = info['id'] # robust module guessing
|
||||
version = info['ver']
|
||||
# Prefer version from directory name for disabled packages (preserves 'nightly' literal)
|
||||
# Otherwise use version from package inspection (commit hash for git repos)
|
||||
if version_from_dirname:
|
||||
version = version_from_dirname
|
||||
else:
|
||||
version = info['ver']
|
||||
|
||||
# Get repository URL for both nightly and CNR packages
|
||||
if version == 'nightly':
|
||||
# For nightly packages, get repo URL from git
|
||||
repo_url = git_utils.git_url(fullpath)
|
||||
elif 'url' in info and info['url']:
|
||||
# For CNR packages, get repo URL from pyproject.toml
|
||||
repo_url = info['url']
|
||||
|
||||
return InstalledNodePackage(
|
||||
id=node_id, fullpath=fullpath, disabled=disabled, version=version
|
||||
id=node_id, fullpath=fullpath, disabled=disabled, version=version, repo_url=repo_url
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user