timedinput 1.0.1__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: timedinput
3
- Version: 1.0.1
3
+ Version: 2.0.0
4
4
  Summary: Timeout for python inputs
5
5
  Author-email: Kerollos Emad <kerollos.em@gmail.com>
6
6
  License: MIT License
@@ -43,6 +43,7 @@ Classifier: License :: OSI Approved :: MIT License
43
43
  Classifier: Operating System :: POSIX :: Linux
44
44
  Classifier: Operating System :: Microsoft :: Windows
45
45
  Classifier: Operating System :: MacOS
46
+ Requires-Python: >=3.7
46
47
  Description-Content-Type: text/markdown
47
48
  License-File: LICENSE
48
49
  Provides-Extra: jupyter
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "timedinput"
7
- version = "1.0.1"
7
+ version = "2.0.0"
8
8
  description = "Timeout for python inputs"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -30,6 +30,7 @@ classifiers = [
30
30
  'Operating System :: Microsoft :: Windows',
31
31
  'Operating System :: MacOS',
32
32
  ]
33
+ requires-python = ">=3.7"
33
34
 
34
35
  [project.urls]
35
36
  "Source" = "https://github.com/kerollosy/timedinput"
@@ -0,0 +1,23 @@
1
+ import time
2
+ import pytest
3
+ from unittest.mock import patch
4
+ from timedinput import timedinput, TimeoutOccurred
5
+
6
+
7
+ def delayed_input(*args, **kwargs):
8
+ """Simulate a user staring at the screen and doing nothing."""
9
+ time.sleep(10)
10
+ return ""
11
+
12
+
13
+ @patch('builtins.input', delayed_input)
14
+ def test_noinput():
15
+ # Will pause for 1 second, get interrupted by the timeout, and return "k"
16
+ assert timedinput("??", timeout=1, default="k") == "k"
17
+
18
+
19
+ @patch('builtins.input', delayed_input)
20
+ def test_timeout():
21
+ # Will pause for 1 second, get interrupted, and raise the exception
22
+ with pytest.raises(TimeoutOccurred):
23
+ timedinput("??", timeout=1)
@@ -1 +1,3 @@
1
1
  from .timedinput import timedinput, TimeoutOccurred
2
+
3
+ __all__ = ["timedinput", "TimeoutOccurred"]
@@ -0,0 +1,245 @@
1
+ """Timeout for python inputs"""
2
+
3
+ import sys
4
+ import time
5
+ import warnings
6
+
7
+ if sys.platform.startswith("win"):
8
+ import msvcrt
9
+ else:
10
+ import readline # noqa: F401 - readline is imported for its side effects on Unix
11
+ import signal
12
+ import termios
13
+
14
+
15
+ DEFAULT_TIMEOUT = 30.0
16
+ INTERVAL = 0.05
17
+
18
+ SP = ' '
19
+ CR = '\r'
20
+ LF = '\n'
21
+ CRLF = CR + LF
22
+
23
+
24
+ class TimeoutOccurred(Exception):
25
+ """Raised when the user doesn't enter input within the specified timeout
26
+ and no default value was provided."""
27
+
28
+
29
+ def is_jupyter():
30
+ """Detects if the code is running inside a Jupyter Notebook, Google Colab, or IPython."""
31
+ if 'google.colab' in sys.modules:
32
+ return True
33
+
34
+ try:
35
+ shell = get_ipython() # noqa: F821 - get_ipython is a special function available in IPython environments
36
+ if shell is None:
37
+ return False
38
+
39
+ return shell.__class__.__name__ in ('ZMQInteractiveShell', 'Shell')
40
+ except Exception:
41
+ return False
42
+
43
+
44
+ def jupyter_timedinput(prompt='', timeout=DEFAULT_TIMEOUT, default=None):
45
+ """Timed input for Jupyter using jupyter_ui_poll."""
46
+ import ipywidgets as widgets
47
+ from IPython.display import display
48
+ from jupyter_ui_poll import ui_events
49
+
50
+ result = [None]
51
+ done = [False]
52
+
53
+ label = widgets.Label(value=prompt)
54
+
55
+ # Add continuous_update=False so it only triggers on Enter
56
+ text = widgets.Text(placeholder='Type and press Enter...', continuous_update=False)
57
+ box = widgets.VBox([label, text])
58
+
59
+ # The callback now receives a 'change' dictionary instead of the widget itself
60
+ def on_submit(change):
61
+ result[0] = change['new'] # Grab the newly typed text
62
+ done[0] = True
63
+
64
+ # Use observe to watch for changes to the 'value'
65
+ text.observe(on_submit, names='value')
66
+ display(box)
67
+
68
+ start_time = time.monotonic()
69
+
70
+ with ui_events() as poll:
71
+ while not done[0]:
72
+ poll(10)
73
+ if time.monotonic() - start_time > timeout:
74
+ done[0] = False
75
+ break
76
+ time.sleep(0.05)
77
+
78
+ box.close()
79
+
80
+ if not done[0]:
81
+ if default is not None:
82
+ return default
83
+ raise TimeoutOccurred
84
+
85
+ return result[0]
86
+
87
+
88
+ def _timeout_handler(signum, frame):
89
+ """Signal handler that raises a timeout exception."""
90
+ raise TimeoutOccurred()
91
+
92
+
93
+ def posix_timedinput(prompt='', timeout=5, default=None):
94
+ """Timedinput for Unix operating systems"""
95
+ old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
96
+
97
+ try:
98
+ signal.setitimer(signal.ITIMER_REAL, timeout)
99
+ result = input(prompt)
100
+ return result
101
+
102
+ except TimeoutOccurred:
103
+ print()
104
+
105
+ try:
106
+ termios.tcflush(sys.stdin, termios.TCIFLUSH)
107
+ except Exception:
108
+ pass
109
+
110
+ if default is not None:
111
+ return default
112
+ raise
113
+
114
+ except EOFError:
115
+ print()
116
+ raise
117
+
118
+ finally:
119
+ signal.setitimer(signal.ITIMER_REAL, 0)
120
+ signal.signal(signal.SIGALRM, old_handler)
121
+
122
+
123
+ def echo(string):
124
+ """Prints a string without a newline and flushes stdout immediately."""
125
+ print(string, end="", flush=True)
126
+
127
+
128
+ def win_timedinput(prompt='', timeout=DEFAULT_TIMEOUT, default=None):
129
+ """Timedinput for Windows operating systems"""
130
+ echo(prompt)
131
+ begin = time.monotonic()
132
+ end = begin + timeout
133
+ line = ''
134
+ pos = 0 # cursor position within line
135
+
136
+ def redraw():
137
+ # Reprint line from start, trailing ' \b' erases any leftover character
138
+ echo('\r' + prompt + line + ' \b')
139
+ back = len(line) - pos
140
+ if back:
141
+ echo('\b' * back)
142
+
143
+ while time.monotonic() < end:
144
+ if msvcrt.kbhit():
145
+ c = msvcrt.getwch()
146
+
147
+ if c in ('\x00', '\xe0'):
148
+ scan = msvcrt.getwch()
149
+ if scan == '\x4b' and pos > 0: # Left arrow
150
+ pos -= 1
151
+ echo('\b')
152
+ elif scan == '\x4d' and pos < len(line): # Right arrow
153
+ echo(line[pos])
154
+ pos += 1
155
+ elif scan == '\x47': # Home
156
+ echo('\b' * pos)
157
+ pos = 0
158
+ elif scan == '\x4f': # End
159
+ echo(line[pos:])
160
+ pos = len(line)
161
+ elif scan == '\x53' and pos < len(line): # Delete key
162
+ line = line[:pos] + line[pos + 1:]
163
+ redraw()
164
+ continue
165
+
166
+ # User pressed Enter
167
+ if c in (CR, LF):
168
+ echo(CRLF)
169
+ return line
170
+
171
+ # User pressed ^C (CTRL+C)
172
+ if c == '\003':
173
+ raise KeyboardInterrupt
174
+
175
+ # User pressed Backspace
176
+ if c == '\b':
177
+ if pos > 0:
178
+ line = line[:pos - 1] + line[pos:]
179
+ pos -= 1
180
+ redraw()
181
+ else:
182
+ line = line[:pos] + c + line[pos:]
183
+ pos += 1
184
+ redraw()
185
+
186
+ time.sleep(INTERVAL)
187
+
188
+ echo(CRLF)
189
+ if default is not None:
190
+ return default
191
+ raise TimeoutOccurred
192
+
193
+
194
+ def _fallback_timedinput(prompt='', _timeout=DEFAULT_TIMEOUT, _default=None):
195
+ """Fallback that consumes extra arguments but acts like standard input"""
196
+ return input(prompt)
197
+
198
+
199
+ def timedinput(prompt='', timeout=DEFAULT_TIMEOUT, default=None):
200
+ """Prompt the user for input with an optional timeout.
201
+
202
+ Automatically selects the correct implementation for the current platform
203
+ (Windows, POSIX, or Jupyter). If the user doesn't respond within the
204
+ timeout, either returns ``default`` or raises ``TimeoutOccurred``.
205
+
206
+ Args:
207
+ prompt (str): Message displayed before the input cursor. Defaults to ''.
208
+ timeout (float): Seconds to wait for input before timing out.
209
+ Defaults to 30.0.
210
+ default: Value to return if the timeout expires. If None and the
211
+ timeout expires, raises TimeoutOccurred. Defaults to None.
212
+
213
+ Returns:
214
+ str: The text entered by the user, or ``default`` if timed out.
215
+
216
+ Raises:
217
+ TimeoutOccurred: If the timeout expires and no default is provided.
218
+ KeyboardInterrupt: If the user presses Ctrl+C (Windows).
219
+ EOFError: If stdin is closed unexpectedly (POSIX).
220
+
221
+ Examples:
222
+ >>> answer = timedinput("Continue? [Y/n]: ", timeout=5, default="Y")
223
+ >>> name = timedinput("Enter your name: ", timeout=10)
224
+ """
225
+ if is_jupyter():
226
+ try:
227
+ return jupyter_timedinput(prompt, timeout, default)
228
+ except ImportError:
229
+ warnings.warn(
230
+ "For timeout support in Jupyter, install optional dependencies: "
231
+ "%pip install ipywidgets jupyter-ui-poll. "
232
+ "Falling back to standard blocking input (timeout and default ignored).",
233
+ stacklevel=2,
234
+ )
235
+ return _fallback_timedinput(prompt, timeout, default)
236
+
237
+ if sys.platform.startswith("win"):
238
+ return win_timedinput(prompt, timeout, default)
239
+
240
+ return posix_timedinput(prompt, timeout, default)
241
+
242
+
243
+ if __name__ == "__main__":
244
+ answer = timedinput("Continue? [Y/n]: ", timeout=5, default="Y")
245
+ print(f"You entered: {answer}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: timedinput
3
- Version: 1.0.1
3
+ Version: 2.0.0
4
4
  Summary: Timeout for python inputs
5
5
  Author-email: Kerollos Emad <kerollos.em@gmail.com>
6
6
  License: MIT License
@@ -43,6 +43,7 @@ Classifier: License :: OSI Approved :: MIT License
43
43
  Classifier: Operating System :: POSIX :: Linux
44
44
  Classifier: Operating System :: Microsoft :: Windows
45
45
  Classifier: Operating System :: MacOS
46
+ Requires-Python: >=3.7
46
47
  Description-Content-Type: text/markdown
47
48
  License-File: LICENSE
48
49
  Provides-Extra: jupyter
@@ -1,11 +0,0 @@
1
- import pytest
2
- from timedinput import timedinput, TimeoutOccurred
3
-
4
-
5
- def test_noinput():
6
- assert timedinput("??", 1, "k") == "k"
7
-
8
-
9
- def test_timeout():
10
- with pytest.raises(TimeoutOccurred):
11
- timedinput("??", 1)
@@ -1,170 +0,0 @@
1
- """Timeout for python inputs"""
2
-
3
- import sys
4
- import time
5
-
6
- if sys.platform.startswith("win"):
7
- import msvcrt
8
- else:
9
- import selectors
10
- import termios
11
-
12
-
13
- DEFAULT_TIMEOUT = 30.0
14
- INTERVAL = 0.05
15
-
16
- SP = ' '
17
- CR = '\r'
18
- LF = '\n'
19
- CRLF = CR + LF
20
-
21
-
22
- class TimeoutOccurred(Exception):
23
- """Gets raised when user doesn't enter
24
- any input within the specified timeout and no default value is specified"""
25
-
26
-
27
- def echo(string):
28
- """Prints a string"""
29
- sys.stdout.write(string)
30
- sys.stdout.flush()
31
-
32
- def is_jupyter():
33
- """Detects if the code is running inside a Jupyter Notebook, Google Colab, or IPython."""
34
- if 'google.colab' in sys.modules:
35
- return True
36
-
37
- try:
38
- shell = get_ipython().__class__.__name__
39
- if shell in ('ZMQInteractiveShell', 'Shell'):
40
- return True
41
- return False
42
- except NameError:
43
- return False
44
-
45
- def jupyter_timedinput(prompt='', timeout=DEFAULT_TIMEOUT, default=None):
46
- """Timed input for Jupyter using jupyter_ui_poll."""
47
- import ipywidgets as widgets
48
- from IPython.display import display
49
- from jupyter_ui_poll import ui_events
50
-
51
- result = [None]
52
- done = [False]
53
-
54
- label = widgets.Label(value=prompt)
55
-
56
- # Add continuous_update=False so it only triggers on Enter
57
- text = widgets.Text(placeholder='Type and press Enter...', continuous_update=False)
58
- box = widgets.VBox([label, text])
59
-
60
- # The callback now receives a 'change' dictionary instead of the widget itself
61
- def on_submit(change):
62
- result[0] = change['new'] # Grab the newly typed text
63
- done[0] = True
64
-
65
- # Use observe to watch for changes to the 'value'
66
- text.observe(on_submit, names='value')
67
- display(box)
68
-
69
- start_time = time.monotonic()
70
-
71
- with ui_events() as poll:
72
- while not done[0]:
73
- poll(10)
74
- if time.monotonic() - start_time > timeout:
75
- break
76
- time.sleep(0.05)
77
-
78
- box.close()
79
-
80
- if not done[0]:
81
- if default is not None:
82
- return default
83
- raise TimeoutOccurred
84
-
85
- return result[0]
86
-
87
-
88
- def posix_timedinput(prompt='', timeout=DEFAULT_TIMEOUT, default=None):
89
- """Timedinput for Unix operating systems"""
90
- echo(prompt)
91
- sel = selectors.DefaultSelector()
92
- sel.register(sys.stdin, selectors.EVENT_READ)
93
- events = sel.select(timeout)
94
-
95
- if events:
96
- key, _ = events[0]
97
- # readline() may return an empty string immediately
98
- result = key.fileobj.readline()
99
- if result:
100
- return result.rstrip(LF)
101
- # If result is empty string, we hit EOF, proceed to timeout logic
102
-
103
- echo(LF)
104
- # tcflush fails on non-TTY
105
- try:
106
- termios.tcflush(sys.stdin, termios.TCIFLUSH)
107
- except Exception:
108
- pass
109
-
110
- if default is not None:
111
- return default
112
- raise TimeoutOccurred
113
-
114
-
115
- def win_timedinput(prompt='', timeout=DEFAULT_TIMEOUT, default=None):
116
- """Timedinput for Windows operating systems"""
117
- echo(prompt)
118
- begin = time.monotonic()
119
- end = begin + timeout
120
- line = ''
121
-
122
- while time.monotonic() < end:
123
- if msvcrt.kbhit():
124
- c = msvcrt.getwche()
125
-
126
- # User pressed Enter
127
- if c in (CR, LF):
128
- echo(CRLF)
129
- return line
130
-
131
- # User pressed ^C (CTRL+C)
132
- if c == '\003':
133
- raise KeyboardInterrupt
134
-
135
- # User pressed Backspace
136
- if c == '\b':
137
- line = line[:-1]
138
- cover = SP * len(prompt + line + SP)
139
- echo(''.join([CR, cover, CR, prompt, line]))
140
-
141
- else:
142
- line += c
143
- time.sleep(INTERVAL)
144
-
145
- echo(CRLF)
146
- if default is not None:
147
- return default
148
- raise TimeoutOccurred
149
-
150
- def _fallback_timedinput(prompt='', timeout=DEFAULT_TIMEOUT, default=None):
151
- """Fallback that consumes extra arguments but acts like standard input"""
152
- return input(prompt)
153
-
154
- if is_jupyter():
155
- try:
156
- import ipywidgets
157
- import jupyter_ui_poll
158
- timedinput = jupyter_timedinput
159
- except ImportError:
160
- import warnings
161
- warnings.warn(
162
- "For timeout support in Jupyter, you must install optional dependencies. "
163
- "Run: %pip install ipywidgets jupyter-ui-poll. "
164
- "Falling back to standard blocking input (no timeout)."
165
- )
166
- timedinput = _fallback_timedinput
167
- elif sys.platform.startswith("win"):
168
- timedinput = win_timedinput
169
- else:
170
- timedinput = posix_timedinput
File without changes
File without changes
File without changes