From 167dfe1643c92611004f6cc1b4c0fbcb1e5dfb7d Mon Sep 17 00:00:00 2001 From: Eshaan Agrawal Date: Wed, 19 Aug 2026 14:04:58 +0545 Subject: [PATCH 1/2] Fix duplicate -c click option parameter conflict Remove the short option '-c' from the --config-file option registration. This resolves a conflict with the --copyright short option '-c', which triggered duplicate parameter registration warnings under click. Signed-off-by: Eshaan Agrawal --- .../scancode-cli/cli-help-text-options.rst | 2 +- docs/source/rst-snippets/cli-core-options.rst | 2 +- src/scancode/cli.py | 2 +- tests/scancode/data/help/help.txt | 45 +++++++++---------- tests/scancode/data/help/help_linux.txt | 45 +++++++++---------- 5 files changed, 47 insertions(+), 49 deletions(-) diff --git a/docs/source/reference/scancode-cli/cli-help-text-options.rst b/docs/source/reference/scancode-cli/cli-help-text-options.rst index bd85727387..caa0a03636 100644 --- a/docs/source/reference/scancode-cli/cli-help-text-options.rst +++ b/docs/source/reference/scancode-cli/cli-help-text-options.rst @@ -173,7 +173,7 @@ The following help text is displayed for ScanCode version 32.0.0: -n, --processes INT Set the number of parallel processes to use. Disable parallel processing if 0. Also disable threading if -1. [default: (number of CPUs)-1] - -c, --config-file FILENAME Path to the configuration file. + --config-file FILENAME Path to the configuration file. -q, --quiet Do not print summary or progress. -v, --verbose Print progress as file-by-file path instead of a progress bar. Print verbose scan counters. diff --git a/docs/source/rst-snippets/cli-core-options.rst b/docs/source/rst-snippets/cli-core-options.rst index 0f589b2c6c..1fb23cf719 100644 --- a/docs/source/rst-snippets/cli-core-options.rst +++ b/docs/source/rst-snippets/cli-core-options.rst @@ -7,7 +7,7 @@ Default: ``(number of CPUs)-1`` --c, --config-file FILENAME Path to the configuration file. +--config-file FILENAME Path to the configuration file. -v, --verbose Print verbose file-by-file progress messages. -q, --quiet Do not print summary or progress messages. diff --git a/src/scancode/cli.py b/src/scancode/cli.py index f7fe221c21..b3db9ac573 100644 --- a/src/scancode/cli.py +++ b/src/scancode/cli.py @@ -233,7 +233,7 @@ def default_processes(): cls=PluggableCommandLineOption, ) -@click.option('-c', '--config-file', +@click.option('--config-file', type=click.File('r'), required=False, help='Path to the configuration file.', diff --git a/tests/scancode/data/help/help.txt b/tests/scancode/data/help/help.txt index 52bc2e0ce7..866802346a 100644 --- a/tests/scancode/data/help/help.txt +++ b/tests/scancode/data/help/help.txt @@ -136,29 +136,28 @@ Options: which are todo items and needs manual review. core: - --ignore Ignore files matching . - --timeout Stop an unfinished file scan after a timeout in - seconds. [default: 120 seconds] - -n, --processes INT Set the number of parallel processes to use. - Disable parallel processing if 0. Also disable - threading if -1. [default: (number of CPUs)-1] - -c, --config-file FILENAME Path to the configuration file. - -q, --quiet Do not print summary or progress. - -v, --verbose Print progress as file-by-file path instead of a - progress bar. Print verbose scan counters. - --from-json Load codebase from one or more JSON scan - file(s). - --max-in-memory INTEGER Maximum number of files and directories scan - details kept in memory during a scan. Additional - files and directories scan details above this - number are cached on-disk rather than in memory. - Use 0 to use unlimited memory and disable on-disk - caching. Use -1 to use only on-disk caching. - [default: 10000] - --max-depth INTEGER Maximum nesting depth of subdirectories to scan. - Descend at most INTEGER levels of directories - below and including the starting directory. Use 0 - for no scan depth limit. + --ignore Ignore files matching . + --timeout Stop an unfinished file scan after a timeout in + seconds. [default: 120 seconds] + -n, --processes INT Set the number of parallel processes to use. Disable + parallel processing if 0. Also disable threading if + -1. [default: (number of CPUs)-1] + --config-file FILENAME Path to the configuration file. + -q, --quiet Do not print summary or progress. + -v, --verbose Print progress as file-by-file path instead of a + progress bar. Print verbose scan counters. + --from-json Load codebase from one or more JSON scan + file(s). + --max-in-memory INTEGER Maximum number of files and directories scan details + kept in memory during a scan. Additional files and + directories scan details above this number are cached + on-disk rather than in memory. Use 0 to use unlimited + memory and disable on-disk caching. Use -1 to use + only on-disk caching. [default: 10000] + --max-depth INTEGER Maximum nesting depth of subdirectories to scan. + Descend at most INTEGER levels of directories below + and including the starting directory. Use 0 for no + scan depth limit. documentation: -h, --help Show this message and exit. diff --git a/tests/scancode/data/help/help_linux.txt b/tests/scancode/data/help/help_linux.txt index 9630f39fb0..22ea0906a8 100644 --- a/tests/scancode/data/help/help_linux.txt +++ b/tests/scancode/data/help/help_linux.txt @@ -138,29 +138,28 @@ Options: which are todo items and needs manual review. core: - --ignore Ignore files matching . - --timeout Stop an unfinished file scan after a timeout in - seconds. [default: 120 seconds] - -n, --processes INT Set the number of parallel processes to use. - Disable parallel processing if 0. Also disable - threading if -1. [default: (number of CPUs)-1] - -c, --config-file FILENAME Path to the configuration file. - -q, --quiet Do not print summary or progress. - -v, --verbose Print progress as file-by-file path instead of a - progress bar. Print verbose scan counters. - --from-json Load codebase from one or more JSON scan - file(s). - --max-in-memory INTEGER Maximum number of files and directories scan - details kept in memory during a scan. Additional - files and directories scan details above this - number are cached on-disk rather than in memory. - Use 0 to use unlimited memory and disable on-disk - caching. Use -1 to use only on-disk caching. - [default: 10000] - --max-depth INTEGER Maximum nesting depth of subdirectories to scan. - Descend at most INTEGER levels of directories - below and including the starting directory. Use 0 - for no scan depth limit. + --ignore Ignore files matching . + --timeout Stop an unfinished file scan after a timeout in + seconds. [default: 120 seconds] + -n, --processes INT Set the number of parallel processes to use. Disable + parallel processing if 0. Also disable threading if + -1. [default: (number of CPUs)-1] + --config-file FILENAME Path to the configuration file. + -q, --quiet Do not print summary or progress. + -v, --verbose Print progress as file-by-file path instead of a + progress bar. Print verbose scan counters. + --from-json Load codebase from one or more JSON scan + file(s). + --max-in-memory INTEGER Maximum number of files and directories scan details + kept in memory during a scan. Additional files and + directories scan details above this number are cached + on-disk rather than in memory. Use 0 to use unlimited + memory and disable on-disk caching. Use -1 to use + only on-disk caching. [default: 10000] + --max-depth INTEGER Maximum nesting depth of subdirectories to scan. + Descend at most INTEGER levels of directories below + and including the starting directory. Use 0 for no + scan depth limit. documentation: -h, --help Show this message and exit. From 43f02c6cc6c34496041b74b0f39e1c1f37fce387 Mon Sep 17 00:00:00 2001 From: Eshaan Agrawal Date: Wed, 19 Aug 2026 15:05:15 +0545 Subject: [PATCH 2/2] Handle ValueError in signal registration on Python 3.14+ Catch ValueError raised when calling signal.signal from a non-main thread or non-main interpreter context (e.g. sub-interpreters on Python 3.14+). Falls back to synchronous execution without a timeout. Signed-off-by: Eshaan Agrawal --- src/scancode/interrupt.py | 158 +++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 77 deletions(-) diff --git a/src/scancode/interrupt.py b/src/scancode/interrupt.py index e26d090567..968279b12b 100644 --- a/src/scancode/interrupt.py +++ b/src/scancode/interrupt.py @@ -51,6 +51,79 @@ class TimeoutError(Exception): # NOQA NO_ERROR = None NO_VALUE = None +from ctypes import c_long +from ctypes import py_object +from ctypes import pythonapi +from multiprocessing import TimeoutError as MpTimeoutError + +from queue import Empty as Queue_Empty +from queue import Queue +from _thread import start_new_thread + +def async_raise(tid, exctype=Exception): + """ + Raise an Exception in the Thread with id `tid`. Perform cleanup if + needed. + + Based on Killable Threads By Tomer Filiba + from http://tomerfiliba.com/recipes/Thread2/ + license: public domain. + """ + assert isinstance(tid, int), 'Invalid thread id: must an integer' + + tid = c_long(tid) + exception = py_object(Exception) + res = pythonapi.PyThreadState_SetAsyncExc(tid, exception) + if res == 0: + raise ValueError('Invalid thread id.') + elif res != 1: + # if it returns a number greater than one, you're in trouble, + # and you should call it again with exc=NULL to revert the effect + pythonapi.PyThreadState_SetAsyncExc(tid, 0) + raise SystemError('PyThreadState_SetAsyncExc failed.') + +def thread_interruptible(func, args=None, kwargs=None, timeout=DEFAULT_TIMEOUT): + """ + Threads-based interruptible runner. It can work on both Windows and POSIX, + but is not reliable and works only if everything is pickable. + """ + # We run `func` in a thread and block on a queue until timeout + results = Queue() + + def runner(): + """ + Run the func and send results back in a queue as a tuple of + (`error`, `value`) + """ + try: + _res = func(*(args or ()), **(kwargs or {})) + results.put((NO_ERROR, _res,)) + except Exception: + results.put((ERROR_MSG + traceback_format_exc(), NO_VALUE,)) + + tid = start_new_thread(runner, ()) + + try: + # wait for the queue results up to timeout + err_res = results.get(timeout=timeout) + + if not err_res: + return ERROR_MSG, NO_VALUE + + return err_res + + except (Queue_Empty, MpTimeoutError): + return TIMEOUT_MSG % locals(), NO_VALUE + + except Exception: + return ERROR_MSG + traceback_format_exc(), NO_VALUE + + finally: + try: + async_raise(tid, Exception) + except (SystemExit, ValueError): + pass + if not on_windows: """ Some code based in part and inspired from the RobotFramework and @@ -93,92 +166,23 @@ def handler(signum, frame): except TimeoutError: return TIMEOUT_MSG % locals(), NO_VALUE - except Exception: + except ValueError as ve: + if 'signal only works in main thread' in str(ve): + # Fallback to the thread-based implementation if we are not in the main thread of the main interpreter + return thread_interruptible(func, args, kwargs, timeout) return ERROR_MSG + traceback_format_exc(), NO_VALUE - finally: - setitimer(ITIMER_REAL, 0) - -elif on_windows: - """ - Run a function in an interruptible thread with a timeout. - Based on an idea of dano "Dan O'Reilly" - http://stackoverflow.com/users/2073595/dano - But no code has been reused from this post. - """ - - from ctypes import c_long - from ctypes import py_object - from ctypes import pythonapi - from multiprocessing import TimeoutError as MpTimeoutError - - from queue import Empty as Queue_Empty - from queue import Queue - from _thread import start_new_thread - - def interruptible(func, args=None, kwargs=None, timeout=DEFAULT_TIMEOUT): - """ - Windows, threads-based interruptible runner. It can work also on - POSIX, but is not reliable and works only if everything is pickable. - """ - # We run `func` in a thread and block on a queue until timeout - results = Queue() - - def runner(): - """ - Run the func and send results back in a queue as a tuple of - (`error`, `value`) - """ - try: - _res = func(*(args or ()), **(kwargs or {})) - results.put((NO_ERROR, _res,)) - except Exception: - results.put((ERROR_MSG + traceback_format_exc(), NO_VALUE,)) - - tid = start_new_thread(runner, ()) - - try: - # wait for the queue results up to timeout - err_res = results.get(timeout=timeout) - - if not err_res: - return ERROR_MSG, NO_VALUE - - return err_res - - except (Queue_Empty, MpTimeoutError): - return TIMEOUT_MSG % locals(), NO_VALUE - except Exception: return ERROR_MSG + traceback_format_exc(), NO_VALUE finally: try: - async_raise(tid, Exception) - except (SystemExit, ValueError): + setitimer(ITIMER_REAL, 0) + except ValueError: pass - def async_raise(tid, exctype=Exception): - """ - Raise an Exception in the Thread with id `tid`. Perform cleanup if - needed. - - Based on Killable Threads By Tomer Filiba - from http://tomerfiliba.com/recipes/Thread2/ - license: public domain. - """ - assert isinstance(tid, int), 'Invalid thread id: must an integer' - - tid = c_long(tid) - exception = py_object(Exception) - res = pythonapi.PyThreadState_SetAsyncExc(tid, exception) - if res == 0: - raise ValueError('Invalid thread id.') - elif res != 1: - # if it returns a number greater than one, you're in trouble, - # and you should call it again with exc=NULL to revert the effect - pythonapi.PyThreadState_SetAsyncExc(tid, 0) - raise SystemError('PyThreadState_SetAsyncExc failed.') +elif on_windows: + interruptible = thread_interruptible def fake_interruptible(func, args=None, kwargs=None, timeout=DEFAULT_TIMEOUT):