diff --git a/dash/_hooks.py b/dash/_hooks.py index 058f0f7e12..7a0c5c8f83 100644 --- a/dash/_hooks.py +++ b/dash/_hooks.py @@ -244,7 +244,7 @@ def get_hooks(cls, hook: str): return cls.hooks.get_hooks(hook) @classmethod - def register_setuptools(cls): + def register_plugins(cls): if cls._registered: # Only have to register once. return diff --git a/dash/background_callback/managers/diskcache_manager.py b/dash/background_callback/managers/diskcache_manager.py index 094485ad71..ae261a1ced 100644 --- a/dash/background_callback/managers/diskcache_manager.py +++ b/dash/background_callback/managers/diskcache_manager.py @@ -85,9 +85,14 @@ def terminate_job(self, job): pass try: - process.wait(1) + process.wait(2) # Increased timeout except (psutil.TimeoutExpired, psutil.NoSuchProcess): - pass + # Force kill if still running + try: + process.kill() + process.wait(1) + except (psutil.TimeoutExpired, psutil.NoSuchProcess): + pass def terminate_unhealthy_job(self, job): import psutil # pylint: disable=import-outside-toplevel,import-error diff --git a/dash/dash.py b/dash/dash.py index cf6d0f43f1..1dddf541a2 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -642,7 +642,7 @@ def _setup_hooks(self): from ._hooks import HooksManager self._hooks = HooksManager - self._hooks.register_setuptools() + self._hooks.register_plugins() for setup in self._hooks.get_hooks("setup"): setup(self) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 276dbfb0f5..a08295d3cf 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -8,7 +8,7 @@ import argparse import shutil import functools -import pkg_resources +from importlib import resources import yaml from ._r_components_generation import write_class_file @@ -57,7 +57,9 @@ def generate_components( is_windows = sys.platform == "win32" - extract_path = pkg_resources.resource_filename("dash", "extract-meta.js") + # Python 3.8 compatible approach using importlib.resources.path() + with resources.path("dash", "extract-meta.js") as resource_path: + extract_path = str(resource_path) reserved_patterns = "|".join(f"^{p}$" for p in reserved_words) diff --git a/dash/extract-meta.js b/dash/extract-meta.js index 3427ef81d3..347ca45f8a 100755 --- a/dash/extract-meta.js +++ b/dash/extract-meta.js @@ -47,6 +47,7 @@ function getTsConfigCompilerOptions() { let failedBuild = false; const excludedDocProps = ['setProps', 'id', 'className', 'style']; +const errorFiles = []; const isOptional = prop => (prop.getFlags() & ts.SymbolFlags.Optional) !== 0; @@ -91,14 +92,18 @@ function logError(error, filePath) { if (error instanceof Error) { process.stderr.write(error.stack + '\n'); } + if (filePath && !errorFiles.includes(filePath)) { + errorFiles.push(filePath); + } } -function isReservedPropName(propName) { +function isReservedPropName(propName, filepath, baseProp) { reservedPatterns.forEach(reservedPattern => { if (reservedPattern.test(propName)) { - process.stderr.write( - `\nERROR: "${propName}" matches reserved word ` + - `pattern: ${reservedPattern.toString()}\n` + logError( + `\nERROR:${filepath}: "${baseProp ? baseProp + "." : ""}${propName}" matches reserved word ` + + `pattern: ${reservedPattern.toString()}\n`, + filepath ); failedBuild = true; } @@ -140,7 +145,7 @@ function parseJSX(filepath) { const src = fs.readFileSync(filepath); const doc = reactDocs.parse(src); Object.keys(doc.props).forEach(propName => - isReservedPropName(propName) + isReservedPropName(propName, filepath) ); docstringWarning(doc); return doc; @@ -152,6 +157,7 @@ function parseJSX(filepath) { function gatherComponents(sources, components = {}) { const names = []; const filepaths = []; + let currentFilepath = "", currentBasePropName = ""; // For debugging purposes. const gather = filepath => { if (ignorePattern && ignorePattern.test(filepath)) { @@ -166,8 +172,9 @@ function gatherComponents(sources, components = {}) { filepaths.push(filepath); names.push(name); } catch (err) { - process.stderr.write( - `ERROR: Invalid component file ${filepath}: ${err}` + logError( + `ERROR: Invalid component file ${filepath}: ${err}`, + filepath, ); } } @@ -594,7 +601,17 @@ function gatherComponents(sources, components = {}) { properties.forEach(prop => { const name = prop.getName(); - if (isReservedPropName(name)) { + + // Skip symbol properties (e.g., __@iterator@3570, __@asyncIterator@3571, etc.) + // These come from TypeScript's getApparentProperties() including inherited symbols + if (name.startsWith('__@') && /@\d+$/.test(name)) { + return; + } + + if (parentType === null) { + currentBasePropName = name; + } + if (isReservedPropName(name, currentFilepath, currentBasePropName)) { return; } const propType = checker.getTypeOfSymbolAtLocation( @@ -660,6 +677,7 @@ function gatherComponents(sources, components = {}) { }; zipArrays(filepaths, names).forEach(([filepath, name]) => { + currentFilepath = filepath; const source = program.getSourceFile(filepath); const moduleSymbol = checker.getSymbolAtLocation(source); const exports = checker.getExportsOfModule(moduleSymbol); @@ -791,5 +809,9 @@ if (!failedBuild) { process.stdout.write(JSON.stringify(metadata, null, 2)); } else { logError('extract-meta failed'); + logError('Check these files for errors:') + errorFiles.forEach((errorFile) => { + logError(`Error in: ${errorFile}`) + }) process.exit(1); } diff --git a/requirements/install.txt b/requirements/install.txt index df0e1299e3..d5cbde53ca 100644 --- a/requirements/install.txt +++ b/requirements/install.txt @@ -6,4 +6,3 @@ typing_extensions>=4.1.1 requests retrying nest-asyncio -setuptools diff --git a/tests/async_tests/utils.py b/tests/async_tests/utils.py index b7074b0735..22ca222197 100644 --- a/tests/async_tests/utils.py +++ b/tests/async_tests/utils.py @@ -147,7 +147,26 @@ def setup_background_callback_app(manager_name, app_name): for job in manager.running_jobs: manager.terminate_job(job) - shutil.rmtree(cache_directory, ignore_errors=True) + # Wait for processes to actually terminate + import time + + for _ in range(10): # Wait up to 5 seconds + if not manager.running_jobs: + break + time.sleep(0.5) + + # Force cleanup with retry logic + import os + + for _ in range(5): + try: + shutil.rmtree(cache_directory, ignore_errors=False) + break + except OSError: + time.sleep(0.5) + else: + # Final attempt with ignore_errors=True + shutil.rmtree(cache_directory, ignore_errors=True) os.environ.pop("LONG_CALLBACK_MANAGER") os.environ.pop("DISKCACHE_DIR") from dash import page_registry diff --git a/tests/background_callback/utils.py b/tests/background_callback/utils.py index c6386f2680..23790a13a2 100644 --- a/tests/background_callback/utils.py +++ b/tests/background_callback/utils.py @@ -150,7 +150,26 @@ def setup_background_callback_app(manager_name, app_name): for job in manager.running_jobs: manager.terminate_job(job) - shutil.rmtree(cache_directory, ignore_errors=True) + # Wait for processes to actually terminate + import time + + for _ in range(10): # Wait up to 5 seconds + if not manager.running_jobs: + break + time.sleep(0.5) + + # Force cleanup with retry logic + import os + + for _ in range(5): + try: + shutil.rmtree(cache_directory, ignore_errors=False) + break + except OSError: + time.sleep(0.5) + else: + # Final attempt with ignore_errors=True + shutil.rmtree(cache_directory, ignore_errors=True) os.environ.pop("LONG_CALLBACK_MANAGER") os.environ.pop("DISKCACHE_DIR") from dash import page_registry