spyder-copy-current 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miroslav Hruska
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ include assets/spyder_copy_current.png
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: spyder-copy-current
3
+ Version: 0.1.0
4
+ Summary: Spyder plugin to copy the current file path and kernel connection info
5
+ Home-page: https://github.com/hruskamiro/spyder-copy-current
6
+ Author: Miroslav Hruska
7
+ Author-email: hruska.miro@gmail.com
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/hruskamiro/spyder-copy-current
10
+ Project-URL: Issues, https://github.com/hruskamiro/spyder-copy-current/issues
11
+ Keywords: spyder,plugin,ipython,jupyter,clipboard,kernel
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: IPython
14
+ Classifier: Framework :: Jupyter
15
+ Classifier: Environment :: X11 Applications :: Qt
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Topic :: Text Editors :: Integrated Development Environments (IDE)
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: jupyter_client<9,>=7.4.9
31
+ Requires-Dist: spyder<7,>=5.4
32
+ Requires-Dist: qtpy
33
+ Dynamic: license-file
34
+
35
+ # spyder-copy-current
36
+
37
+ Small Spyder plugin that copies the current editor file path and the active IPython kernel connection info to the clipboard.
38
+
39
+ Default shortcuts:
40
+
41
+ - **`Ctrl+Alt+C`**: copy the current file path; press twice for `{"path": "<path>", "cell": "<cell>"}`; press three times for `{"path": "<path>", "cell": "<cell>", "line": <line>}`
42
+ - **`Ctrl+Alt+K`**: copy the active IPython kernel connection info
43
+
44
+ ## Supported Versions
45
+
46
+ - Spyder `5.4.x` through `6.x`
47
+ - Python `3.8+`
48
+
49
+ ## Behavior
50
+
51
+ - Copies the current file path to the system clipboard.
52
+ - Copies `{"path": "<path>", "cell": "<cell>"}` on the second quick press, and `{"path": "<path>", "cell": "<cell>", "line": <line>}` on the third quick press.
53
+ - Copies the current IPython kernel connection info to the system clipboard.
54
+ - Works from the active editor.
55
+ - Works from the active IPython console for kernel connection info.
56
+ - If the current tab has not been saved yet, it shows a message instead of copying anything.
57
+ - If the current IPython console does not expose kernel connection info, it shows a message instead of copying anything.
58
+
59
+ ## Install
60
+
61
+ Inside the same environment where Spyder runs:
62
+
63
+ Install the latest released version from PyPI:
64
+
65
+ ```bash
66
+ pip install spyder-copy-current
67
+ ```
68
+
69
+ Or install directly from GitHub:
70
+
71
+ ```bash
72
+ pip install "git+https://github.com/hruskamiro/spyder-copy-current.git@main"
73
+ ```
74
+
75
+ Then restart Spyder.
76
+
77
+ ## Compatibility Notes
78
+
79
+ - The plugin is intended for Spyder `5.4.x` through `6.x`.
80
+ - Existing user-defined Spyder shortcuts are preserved.
81
+ - The default shortcuts are only assigned if the corresponding Spyder shortcut entry is still empty.
82
+
83
+ ## Usage
84
+
85
+ - Press `Ctrl+Alt+C` in the editor to copy the current file path. Press it twice in quick succession to copy `{"path": "<path>", "cell": "<cell>"}`; press it three times to include the cursor line number.
86
+ - Press `Ctrl+Alt+K` in the IPython console to copy the current kernel connection info.
87
+
88
+ The kernel connection info is copied in a `%connect_info`-style format so it can be reused with Jupyter clients when available.
89
+
90
+ ## Shortcut
91
+
92
+ The plugin registers a keyboard shortcut in Spyder's shortcuts preferences under the editor context.
93
+ If that shortcut is still empty, it assigns `Ctrl+Alt+C` by default. Repeating it quickly adds the nearest cell marker, then the cursor line number.
94
+ It also registers a keyboard shortcut in the IPython console context and assigns `Ctrl+Alt+K` by default if that shortcut is still empty.
@@ -0,0 +1,60 @@
1
+ # spyder-copy-current
2
+
3
+ Small Spyder plugin that copies the current editor file path and the active IPython kernel connection info to the clipboard.
4
+
5
+ Default shortcuts:
6
+
7
+ - **`Ctrl+Alt+C`**: copy the current file path; press twice for `{"path": "<path>", "cell": "<cell>"}`; press three times for `{"path": "<path>", "cell": "<cell>", "line": <line>}`
8
+ - **`Ctrl+Alt+K`**: copy the active IPython kernel connection info
9
+
10
+ ## Supported Versions
11
+
12
+ - Spyder `5.4.x` through `6.x`
13
+ - Python `3.8+`
14
+
15
+ ## Behavior
16
+
17
+ - Copies the current file path to the system clipboard.
18
+ - Copies `{"path": "<path>", "cell": "<cell>"}` on the second quick press, and `{"path": "<path>", "cell": "<cell>", "line": <line>}` on the third quick press.
19
+ - Copies the current IPython kernel connection info to the system clipboard.
20
+ - Works from the active editor.
21
+ - Works from the active IPython console for kernel connection info.
22
+ - If the current tab has not been saved yet, it shows a message instead of copying anything.
23
+ - If the current IPython console does not expose kernel connection info, it shows a message instead of copying anything.
24
+
25
+ ## Install
26
+
27
+ Inside the same environment where Spyder runs:
28
+
29
+ Install the latest released version from PyPI:
30
+
31
+ ```bash
32
+ pip install spyder-copy-current
33
+ ```
34
+
35
+ Or install directly from GitHub:
36
+
37
+ ```bash
38
+ pip install "git+https://github.com/hruskamiro/spyder-copy-current.git@main"
39
+ ```
40
+
41
+ Then restart Spyder.
42
+
43
+ ## Compatibility Notes
44
+
45
+ - The plugin is intended for Spyder `5.4.x` through `6.x`.
46
+ - Existing user-defined Spyder shortcuts are preserved.
47
+ - The default shortcuts are only assigned if the corresponding Spyder shortcut entry is still empty.
48
+
49
+ ## Usage
50
+
51
+ - Press `Ctrl+Alt+C` in the editor to copy the current file path. Press it twice in quick succession to copy `{"path": "<path>", "cell": "<cell>"}`; press it three times to include the cursor line number.
52
+ - Press `Ctrl+Alt+K` in the IPython console to copy the current kernel connection info.
53
+
54
+ The kernel connection info is copied in a `%connect_info`-style format so it can be reused with Jupyter clients when available.
55
+
56
+ ## Shortcut
57
+
58
+ The plugin registers a keyboard shortcut in Spyder's shortcuts preferences under the editor context.
59
+ If that shortcut is still empty, it assigns `Ctrl+Alt+C` by default. Repeating it quickly adds the nearest cell marker, then the cursor line number.
60
+ It also registers a keyboard shortcut in the IPython console context and assigns `Ctrl+Alt+K` by default if that shortcut is still empty.
@@ -0,0 +1,68 @@
1
+ # spyder-copy-current
2
+
3
+ Small Spyder plugin that copies the current editor file path and the active IPython kernel connection info to the clipboard.
4
+
5
+ Default shortcuts:
6
+
7
+ - **`Ctrl+Alt+C`**: copy the current file path; press twice for `{"path": "<path>", "cell": "<cell>"}`; press three times for `{"path": "<path>", "cell": "<cell>", "line": <line>}`
8
+ - **`Ctrl+Alt+K`**: copy the active IPython kernel connection info
9
+
10
+ <!-- pypi-remove-start -->
11
+ ## Screenshot
12
+
13
+ ![Spyder Copy Current](assets/spyder_copy_current.png)
14
+
15
+ <!-- pypi-remove-end -->
16
+
17
+
18
+ ## Supported Versions
19
+
20
+ - Spyder `5.4.x` through `6.x`
21
+ - Python `3.8+`
22
+
23
+ ## Behavior
24
+
25
+ - Copies the current file path to the system clipboard.
26
+ - Copies `{"path": "<path>", "cell": "<cell>"}` on the second quick press, and `{"path": "<path>", "cell": "<cell>", "line": <line>}` on the third quick press.
27
+ - Copies the current IPython kernel connection info to the system clipboard.
28
+ - Works from the active editor.
29
+ - Works from the active IPython console for kernel connection info.
30
+ - If the current tab has not been saved yet, it shows a message instead of copying anything.
31
+ - If the current IPython console does not expose kernel connection info, it shows a message instead of copying anything.
32
+
33
+ ## Install
34
+
35
+ Inside the same environment where Spyder runs:
36
+
37
+ Install the latest released version from PyPI:
38
+
39
+ ```bash
40
+ pip install spyder-copy-current
41
+ ```
42
+
43
+ Or install directly from GitHub:
44
+
45
+ ```bash
46
+ pip install "git+https://github.com/hruskamiro/spyder-copy-current.git@main"
47
+ ```
48
+
49
+ Then restart Spyder.
50
+
51
+ ## Compatibility Notes
52
+
53
+ - The plugin is intended for Spyder `5.4.x` through `6.x`.
54
+ - Existing user-defined Spyder shortcuts are preserved.
55
+ - The default shortcuts are only assigned if the corresponding Spyder shortcut entry is still empty.
56
+
57
+ ## Usage
58
+
59
+ - Press `Ctrl+Alt+C` in the editor to copy the current file path. Press it twice in quick succession to copy `{"path": "<path>", "cell": "<cell>"}`; press it three times to include the cursor line number.
60
+ - Press `Ctrl+Alt+K` in the IPython console to copy the current kernel connection info.
61
+
62
+ The kernel connection info is copied in a `%connect_info`-style format so it can be reused with Jupyter clients when available.
63
+
64
+ ## Shortcut
65
+
66
+ The plugin registers a keyboard shortcut in Spyder's shortcuts preferences under the editor context.
67
+ If that shortcut is still empty, it assigns `Ctrl+Alt+C` by default. Repeating it quickly adds the nearest cell marker, then the cursor line number.
68
+ It also registers a keyboard shortcut in the IPython console context and assigns `Ctrl+Alt+K` by default if that shortcut is still empty.
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=45", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,52 @@
1
+ [metadata]
2
+ name = spyder-copy-current
3
+ version = 0.1.0
4
+ description = Spyder plugin to copy the current file path and kernel connection info
5
+ long_description = file: README-pypi.md
6
+ long_description_content_type = text/markdown
7
+ author = Miroslav Hruska
8
+ author_email = hruska.miro@gmail.com
9
+ license = MIT
10
+ license_files = LICENSE
11
+ url = https://github.com/hruskamiro/spyder-copy-current
12
+ project_urls =
13
+ Source = https://github.com/hruskamiro/spyder-copy-current
14
+ Issues = https://github.com/hruskamiro/spyder-copy-current/issues
15
+ keywords = spyder, plugin, ipython, jupyter, clipboard, kernel
16
+ classifiers =
17
+ Development Status :: 3 - Alpha
18
+ Framework :: IPython
19
+ Framework :: Jupyter
20
+ Environment :: X11 Applications :: Qt
21
+ Intended Audience :: Developers
22
+ Operating System :: OS Independent
23
+ Programming Language :: Python
24
+ Programming Language :: Python :: 3
25
+ Programming Language :: Python :: 3.8
26
+ Programming Language :: Python :: 3.9
27
+ Programming Language :: Python :: 3.10
28
+ Programming Language :: Python :: 3.11
29
+ Programming Language :: Python :: 3.12
30
+ Topic :: Software Development :: Libraries :: Python Modules
31
+ Topic :: Text Editors :: Integrated Development Environments (IDE)
32
+
33
+ [options]
34
+ packages = find:
35
+ python_requires = >=3.8
36
+ install_requires =
37
+ jupyter_client>=7.4.9,<9
38
+ spyder>=5.4,<7
39
+ qtpy
40
+ include_package_data = True
41
+
42
+ [options.packages.find]
43
+ include = spyder_copy_current*
44
+
45
+ [options.entry_points]
46
+ spyder.plugins =
47
+ spyder_copy_current = spyder_copy_current.spyder.plugin:CopyCurrentPlugin
48
+
49
+ [egg_info]
50
+ tag_build =
51
+ tag_date = 0
52
+
@@ -0,0 +1,4 @@
1
+ from setuptools import setup
2
+
3
+
4
+ setup()
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,592 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Spyder plugin to copy the current editor file path."""
3
+
4
+ import json
5
+ import inspect
6
+ import os
7
+ import re
8
+ import time
9
+
10
+ from jupyter_client.connect import find_connection_file
11
+ from qtpy.QtGui import QGuiApplication, QIcon
12
+ from qtpy.QtWidgets import QMessageBox
13
+
14
+ from spyder.api.plugins import Plugins, SpyderPluginV2
15
+
16
+
17
+ class CopyCurrentPlugin(SpyderPluginV2):
18
+ """Plugin that copies current editor and kernel info to the clipboard."""
19
+
20
+ NAME = "spyder_copy_current"
21
+ CONF_SECTION = NAME
22
+ CONTEXT_NAME = NAME
23
+
24
+ REQUIRES = [Plugins.Editor]
25
+ OPTIONAL = [plugin for plugin in (Plugins.Shortcuts, getattr(Plugins, "IPythonConsole", None)) if plugin is not None]
26
+
27
+ REPEAT_PRESS_SECONDS = 2.0
28
+ CELL_MARKER_RE = re.compile(r"^\s*#\s*%%\s*(.*)$")
29
+
30
+ @staticmethod
31
+ def get_name():
32
+ return "Copy Current"
33
+
34
+ @staticmethod
35
+ def get_description():
36
+ return "Copy the current editor file path or kernel info to the clipboard."
37
+
38
+ @staticmethod
39
+ def get_icon():
40
+ return QIcon()
41
+
42
+ def on_initialize(self):
43
+ self._file_path_copy_state = (None, 0.0, 0)
44
+
45
+ self.create_action(
46
+ name="copy_current_file_path",
47
+ text="Copy current file path",
48
+ tip="Copy the current editor file path to the clipboard",
49
+ triggered=self.copy_current_file_path,
50
+ shortcut_context="editor",
51
+ register_shortcut=True,
52
+ )
53
+
54
+ self.create_action(
55
+ name="copy_current_kernel_connection_info",
56
+ text="Copy current kernel connection info",
57
+ tip="Copy the current IPython kernel connection info to the clipboard",
58
+ triggered=self.copy_current_kernel_connection_info,
59
+ shortcut_context="ipython_console",
60
+ register_shortcut=True,
61
+ )
62
+
63
+ self.main.addAction(self.get_action("copy_current_file_path"))
64
+ self.main.addAction(self.get_action("copy_current_kernel_connection_info"))
65
+
66
+ def on_mainwindow_visible(self):
67
+ shortcuts = self.get_plugin(Plugins.Shortcuts)
68
+ if shortcuts is None:
69
+ return
70
+
71
+ def _uses_spyder5_shortcuts_api():
72
+ try:
73
+ params = list(inspect.signature(shortcuts.set_shortcut).parameters)
74
+ return bool(params) and params[0] == "context"
75
+ except Exception:
76
+ return False
77
+
78
+ def _get_shortcut(context, name):
79
+ try:
80
+ if _uses_spyder5_shortcuts_api():
81
+ return shortcuts.get_shortcut(context, name)
82
+ return shortcuts.get_shortcut(name, context)
83
+ except Exception:
84
+ return ""
85
+
86
+ def _set_shortcut(context, name, value):
87
+ try:
88
+ if _uses_spyder5_shortcuts_api():
89
+ shortcuts.set_shortcut(context, name, value)
90
+ else:
91
+ shortcuts.set_shortcut(value, name, context)
92
+ shortcuts.apply_shortcuts()
93
+ except Exception:
94
+ pass
95
+
96
+ if not _get_shortcut("editor", "copy_current_file_path"):
97
+ _set_shortcut("editor", "copy_current_file_path", "Ctrl+Alt+C")
98
+
99
+ if not _get_shortcut(
100
+ "ipython_console",
101
+ "copy_current_kernel_connection_info",
102
+ ):
103
+ _set_shortcut(
104
+ "ipython_console",
105
+ "copy_current_kernel_connection_info",
106
+ "Ctrl+Alt+K",
107
+ )
108
+
109
+ def _get_editorstack(self):
110
+ editor_plugin = self.get_plugin(Plugins.Editor)
111
+ if editor_plugin is None:
112
+ return None
113
+
114
+ try:
115
+ return editor_plugin.get_current_editorstack()
116
+ except Exception:
117
+ return None
118
+
119
+ @staticmethod
120
+ def _get_current_editor(stack):
121
+ if stack is None:
122
+ return None
123
+
124
+ getter = getattr(stack, "get_current_editor", None)
125
+ if callable(getter):
126
+ try:
127
+ editor = getter()
128
+ except Exception:
129
+ editor = None
130
+ if editor is not None:
131
+ return editor
132
+
133
+ return None
134
+
135
+ @staticmethod
136
+ def _get_editor_text(editor):
137
+ if editor is None:
138
+ return None
139
+
140
+ for method_name in ("toPlainText", "get_text"):
141
+ method = getattr(editor, method_name, None)
142
+ if callable(method):
143
+ try:
144
+ value = method()
145
+ except TypeError:
146
+ continue
147
+ except Exception:
148
+ value = None
149
+ if isinstance(value, str):
150
+ return value
151
+
152
+ value = getattr(editor, "text", None)
153
+ if isinstance(value, str):
154
+ return value
155
+
156
+ return None
157
+
158
+ @staticmethod
159
+ def _get_cursor_line_number(editor):
160
+ if editor is None:
161
+ return None
162
+
163
+ cursor_getter = getattr(editor, "textCursor", None)
164
+ if callable(cursor_getter):
165
+ try:
166
+ cursor = cursor_getter()
167
+ return cursor.blockNumber() + 1
168
+ except Exception:
169
+ pass
170
+
171
+ for method_name in ("get_cursor_line_number", "get_cursor_line"):
172
+ method = getattr(editor, method_name, None)
173
+ if callable(method):
174
+ try:
175
+ value = method()
176
+ except Exception:
177
+ continue
178
+
179
+ if isinstance(value, tuple):
180
+ value = value[0] if value else None
181
+
182
+ if isinstance(value, int):
183
+ return value if value >= 1 else value + 1
184
+
185
+ return None
186
+
187
+ @classmethod
188
+ def _get_current_cell_marker_line(cls, editor, line_number):
189
+ text = cls._get_editor_text(editor)
190
+ if not text or not line_number:
191
+ return "(unknown)"
192
+
193
+ lines = text.splitlines()
194
+ index = min(max(line_number, 1), len(lines)) - 1
195
+ for line in reversed(lines[: index + 1]):
196
+ if cls.CELL_MARKER_RE.match(line):
197
+ return line.strip() or "# %%"
198
+
199
+ return "(no cell)"
200
+
201
+ @classmethod
202
+ def _get_current_location_parts(cls, stack, path):
203
+ editor = cls._get_current_editor(stack)
204
+ line_number = cls._get_cursor_line_number(editor)
205
+ cell = cls._get_current_cell_marker_line(editor, line_number)
206
+ return path, cell, line_number
207
+
208
+ @classmethod
209
+ def _format_current_location(cls, stack, path, include_line):
210
+ full_path, cell, line_number = cls._get_current_location_parts(
211
+ stack,
212
+ path,
213
+ )
214
+ payload = {"path": full_path, "cell": cell}
215
+ if include_line:
216
+ payload["line"] = line_number
217
+ return json.dumps(payload, separators=(",", ": "))
218
+
219
+ @staticmethod
220
+ def _path_from_finfo(finfo):
221
+ if finfo is None:
222
+ return None
223
+
224
+ for attr in ("filename", "path"):
225
+ value = getattr(finfo, attr, None)
226
+ if isinstance(value, str) and value:
227
+ return value
228
+
229
+ getter = getattr(finfo, "get_filename", None)
230
+ if callable(getter):
231
+ try:
232
+ value = getter()
233
+ if isinstance(value, str) and value:
234
+ return value
235
+ except Exception:
236
+ pass
237
+
238
+ return None
239
+
240
+ @classmethod
241
+ def _get_current_path(cls, stack):
242
+ if stack is None:
243
+ return None
244
+
245
+ for method_name in ("get_current_finfo", "get_current_filename"):
246
+ method = getattr(stack, method_name, None)
247
+ if callable(method):
248
+ try:
249
+ value = method()
250
+ except Exception:
251
+ continue
252
+
253
+ if isinstance(value, str) and value:
254
+ return value
255
+
256
+ path = cls._path_from_finfo(value)
257
+ if path:
258
+ return path
259
+
260
+ current_finfo = getattr(stack, "data", None)
261
+ current_index = getattr(stack, "get_stack_index", None)
262
+ if isinstance(current_finfo, list) and callable(current_index):
263
+ try:
264
+ index = current_index()
265
+ if 0 <= index < len(current_finfo):
266
+ path = cls._path_from_finfo(current_finfo[index])
267
+ if path:
268
+ return path
269
+ except Exception:
270
+ pass
271
+
272
+ editor = None
273
+ getter = getattr(stack, "get_current_editor", None)
274
+ if callable(getter):
275
+ try:
276
+ editor = getter()
277
+ except Exception:
278
+ editor = None
279
+
280
+ if editor is not None:
281
+ for attr in ("filename", "path"):
282
+ value = getattr(editor, attr, None)
283
+ if isinstance(value, str) and value:
284
+ return value
285
+
286
+ return None
287
+
288
+ def _get_ipython_console_plugin(self):
289
+ ipython_console = getattr(Plugins, "IPythonConsole", None)
290
+ if ipython_console is None:
291
+ return None
292
+
293
+ try:
294
+ return self.get_plugin(ipython_console)
295
+ except Exception:
296
+ return None
297
+
298
+ @staticmethod
299
+ def _call_first(obj, method_names):
300
+ for method_name in method_names:
301
+ method = getattr(obj, method_name, None)
302
+ if callable(method):
303
+ try:
304
+ value = method()
305
+ except Exception:
306
+ continue
307
+ if value is not None:
308
+ return value
309
+
310
+ return None
311
+
312
+ def _get_current_console_client(self, ipyconsole):
313
+ if ipyconsole is None:
314
+ return None
315
+
316
+ return self._call_first(
317
+ ipyconsole,
318
+ (
319
+ "get_current_client",
320
+ "get_focus_client",
321
+ "get_current_widget",
322
+ ),
323
+ )
324
+
325
+ def _get_current_shellwidget(self, ipyconsole, client):
326
+ shellwidget = self._call_first(
327
+ ipyconsole,
328
+ (
329
+ "get_current_shellwidget",
330
+ "get_focus_shellwidget",
331
+ ),
332
+ )
333
+ if shellwidget is not None:
334
+ return shellwidget
335
+
336
+ if client is None:
337
+ return None
338
+
339
+ for attr in ("shellwidget", "_shellwidget"):
340
+ value = getattr(client, attr, None)
341
+ if value is not None:
342
+ return value
343
+
344
+ return self._call_first(client, ("get_shellwidget",))
345
+
346
+ @staticmethod
347
+ def _resolve_connection_file(ipyconsole, connection_file):
348
+ if not connection_file:
349
+ return None
350
+
351
+ if os.path.isfile(connection_file):
352
+ return connection_file
353
+
354
+ widget_getter = getattr(ipyconsole, "get_widget", None)
355
+ if callable(widget_getter):
356
+ try:
357
+ widget = widget_getter()
358
+ except Exception:
359
+ widget = None
360
+
361
+ finder = getattr(widget, "find_connection_file", None)
362
+ if callable(finder):
363
+ try:
364
+ resolved = finder(connection_file)
365
+ except Exception:
366
+ resolved = None
367
+
368
+ if resolved and os.path.isfile(resolved):
369
+ return resolved
370
+
371
+ try:
372
+ resolved = find_connection_file(connection_file)
373
+ except Exception:
374
+ resolved = None
375
+
376
+ if resolved and os.path.isfile(resolved):
377
+ return resolved
378
+
379
+ return connection_file
380
+
381
+ @staticmethod
382
+ def _read_connection_info_from_file(connection_file):
383
+ if not connection_file or not os.path.isfile(connection_file):
384
+ return None
385
+
386
+ try:
387
+ with open(connection_file, "r", encoding="utf-8") as handle:
388
+ return json.load(handle)
389
+ except Exception:
390
+ return None
391
+
392
+ @staticmethod
393
+ def _get_connection_file(shellwidget, client, kernel_client):
394
+ for obj in (shellwidget, client, kernel_client):
395
+ if obj is None:
396
+ continue
397
+
398
+ getter = getattr(obj, "get_connection_file", None)
399
+ if callable(getter):
400
+ try:
401
+ value = getter()
402
+ except Exception:
403
+ value = None
404
+ if isinstance(value, str) and value:
405
+ return value
406
+
407
+ for attr in ("connection_file", "_connection_file"):
408
+ value = getattr(obj, attr, None)
409
+ if isinstance(value, str) and value:
410
+ return value
411
+
412
+ return None
413
+
414
+ @staticmethod
415
+ def _get_kernel_client(shellwidget, client):
416
+ for obj in (shellwidget, client):
417
+ if obj is None:
418
+ continue
419
+
420
+ getter = getattr(obj, "get_kernel_client", None)
421
+ if callable(getter):
422
+ try:
423
+ value = getter()
424
+ except Exception:
425
+ value = None
426
+ if value is not None:
427
+ return value
428
+
429
+ for attr in ("kernel_client", "_kernel_client"):
430
+ value = getattr(obj, attr, None)
431
+ if value is not None:
432
+ return value
433
+
434
+ return None
435
+
436
+ @staticmethod
437
+ def _get_connection_info(kernel_client, connection_file):
438
+ if kernel_client is not None:
439
+ getter = getattr(kernel_client, "get_connection_info", None)
440
+ if callable(getter):
441
+ for kwargs in ({"session": False}, {}):
442
+ try:
443
+ value = getter(**kwargs)
444
+ except TypeError:
445
+ continue
446
+ except Exception:
447
+ value = None
448
+
449
+ if isinstance(value, bytes):
450
+ try:
451
+ value = value.decode("utf-8")
452
+ except Exception:
453
+ value = None
454
+
455
+ if isinstance(value, str) and value.strip():
456
+ try:
457
+ return json.loads(value)
458
+ except Exception:
459
+ pass
460
+
461
+ if isinstance(value, dict) and value:
462
+ return value
463
+
464
+ return CopyCurrentPlugin._read_connection_info_from_file(connection_file)
465
+
466
+ @staticmethod
467
+ def _make_json_safe(value):
468
+ if isinstance(value, bytes):
469
+ try:
470
+ return value.decode("utf-8")
471
+ except Exception:
472
+ return value.decode("utf-8", errors="replace")
473
+
474
+ if isinstance(value, dict):
475
+ return {
476
+ CopyCurrentPlugin._make_json_safe(key): CopyCurrentPlugin._make_json_safe(item)
477
+ for key, item in value.items()
478
+ }
479
+
480
+ if isinstance(value, (list, tuple)):
481
+ return [CopyCurrentPlugin._make_json_safe(item) for item in value]
482
+
483
+ return value
484
+
485
+ @staticmethod
486
+ def _format_connection_info(connection_info, connection_file):
487
+ safe_info = CopyCurrentPlugin._make_json_safe(connection_info)
488
+ body = json.dumps(safe_info, indent=2, sort_keys=True)
489
+ lines = [body]
490
+ if connection_file:
491
+ lines.extend(
492
+ (
493
+ "",
494
+ f"Connection file: {connection_file}",
495
+ "Connect with:",
496
+ f" jupyter console --existing {connection_file}",
497
+ )
498
+ )
499
+ return "\n".join(lines)
500
+
501
+ def _set_clipboard_text(self, title, text, status_message):
502
+ clipboard = QGuiApplication.clipboard()
503
+ if clipboard is None:
504
+ QMessageBox.warning(
505
+ self.main,
506
+ title,
507
+ "Could not access the clipboard.",
508
+ )
509
+ return False
510
+
511
+ clipboard.setText(text)
512
+ self.show_status_message(status_message, 3000)
513
+ return True
514
+
515
+ def copy_current_file_path(self):
516
+ stack = self._get_editorstack()
517
+ path = self._get_current_path(stack)
518
+
519
+ if not path:
520
+ QMessageBox.information(
521
+ self.main,
522
+ "Copy Current File Path",
523
+ "The current editor tab does not have a saved file path yet.",
524
+ )
525
+ return
526
+
527
+ normalized = os.path.abspath(path)
528
+ last_path, last_time, repeat_count = getattr(
529
+ self,
530
+ "_file_path_copy_state",
531
+ (None, 0.0, 0),
532
+ )
533
+ now = time.monotonic()
534
+
535
+ if (
536
+ last_path == normalized
537
+ and now - last_time <= self.REPEAT_PRESS_SECONDS
538
+ ):
539
+ repeat_count += 1
540
+ else:
541
+ repeat_count = 1
542
+
543
+ self._file_path_copy_state = (normalized, now, repeat_count)
544
+
545
+ if repeat_count == 1:
546
+ self._set_clipboard_text(
547
+ "Copy Current File Path",
548
+ normalized,
549
+ f"Copied file path: {normalized}",
550
+ )
551
+ return
552
+
553
+ include_line = repeat_count >= 3
554
+ payload = self._format_current_location(stack, normalized, include_line)
555
+ self._set_clipboard_text(
556
+ "Copy Current Location",
557
+ payload,
558
+ f"Copied current location: {payload}",
559
+ )
560
+
561
+ def copy_current_kernel_connection_info(self):
562
+ ipyconsole = self._get_ipython_console_plugin()
563
+ client = self._get_current_console_client(ipyconsole)
564
+ shellwidget = self._get_current_shellwidget(ipyconsole, client)
565
+ kernel_client = self._get_kernel_client(shellwidget, client)
566
+ connection_file = self._resolve_connection_file(
567
+ ipyconsole,
568
+ self._get_connection_file(
569
+ shellwidget,
570
+ client,
571
+ kernel_client,
572
+ ),
573
+ )
574
+ connection_info = self._get_connection_info(
575
+ kernel_client,
576
+ connection_file,
577
+ )
578
+
579
+ if not connection_info:
580
+ QMessageBox.information(
581
+ self.main,
582
+ "Copy Kernel Connection Info",
583
+ "Could not find kernel connection info for the current IPython console.",
584
+ )
585
+ return
586
+
587
+ payload = self._format_connection_info(connection_info, connection_file)
588
+ self._set_clipboard_text(
589
+ "Copy Kernel Connection Info",
590
+ payload,
591
+ "Copied current kernel connection info.",
592
+ )
@@ -0,0 +1,4 @@
1
+ """Compatibility wrapper for Spyder plugin entry point."""
2
+
3
+ from .. import __version__
4
+ from ..plugin import CopyCurrentPlugin
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: spyder-copy-current
3
+ Version: 0.1.0
4
+ Summary: Spyder plugin to copy the current file path and kernel connection info
5
+ Home-page: https://github.com/hruskamiro/spyder-copy-current
6
+ Author: Miroslav Hruska
7
+ Author-email: hruska.miro@gmail.com
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/hruskamiro/spyder-copy-current
10
+ Project-URL: Issues, https://github.com/hruskamiro/spyder-copy-current/issues
11
+ Keywords: spyder,plugin,ipython,jupyter,clipboard,kernel
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: IPython
14
+ Classifier: Framework :: Jupyter
15
+ Classifier: Environment :: X11 Applications :: Qt
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Topic :: Text Editors :: Integrated Development Environments (IDE)
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: jupyter_client<9,>=7.4.9
31
+ Requires-Dist: spyder<7,>=5.4
32
+ Requires-Dist: qtpy
33
+ Dynamic: license-file
34
+
35
+ # spyder-copy-current
36
+
37
+ Small Spyder plugin that copies the current editor file path and the active IPython kernel connection info to the clipboard.
38
+
39
+ Default shortcuts:
40
+
41
+ - **`Ctrl+Alt+C`**: copy the current file path; press twice for `{"path": "<path>", "cell": "<cell>"}`; press three times for `{"path": "<path>", "cell": "<cell>", "line": <line>}`
42
+ - **`Ctrl+Alt+K`**: copy the active IPython kernel connection info
43
+
44
+ ## Supported Versions
45
+
46
+ - Spyder `5.4.x` through `6.x`
47
+ - Python `3.8+`
48
+
49
+ ## Behavior
50
+
51
+ - Copies the current file path to the system clipboard.
52
+ - Copies `{"path": "<path>", "cell": "<cell>"}` on the second quick press, and `{"path": "<path>", "cell": "<cell>", "line": <line>}` on the third quick press.
53
+ - Copies the current IPython kernel connection info to the system clipboard.
54
+ - Works from the active editor.
55
+ - Works from the active IPython console for kernel connection info.
56
+ - If the current tab has not been saved yet, it shows a message instead of copying anything.
57
+ - If the current IPython console does not expose kernel connection info, it shows a message instead of copying anything.
58
+
59
+ ## Install
60
+
61
+ Inside the same environment where Spyder runs:
62
+
63
+ Install the latest released version from PyPI:
64
+
65
+ ```bash
66
+ pip install spyder-copy-current
67
+ ```
68
+
69
+ Or install directly from GitHub:
70
+
71
+ ```bash
72
+ pip install "git+https://github.com/hruskamiro/spyder-copy-current.git@main"
73
+ ```
74
+
75
+ Then restart Spyder.
76
+
77
+ ## Compatibility Notes
78
+
79
+ - The plugin is intended for Spyder `5.4.x` through `6.x`.
80
+ - Existing user-defined Spyder shortcuts are preserved.
81
+ - The default shortcuts are only assigned if the corresponding Spyder shortcut entry is still empty.
82
+
83
+ ## Usage
84
+
85
+ - Press `Ctrl+Alt+C` in the editor to copy the current file path. Press it twice in quick succession to copy `{"path": "<path>", "cell": "<cell>"}`; press it three times to include the cursor line number.
86
+ - Press `Ctrl+Alt+K` in the IPython console to copy the current kernel connection info.
87
+
88
+ The kernel connection info is copied in a `%connect_info`-style format so it can be reused with Jupyter clients when available.
89
+
90
+ ## Shortcut
91
+
92
+ The plugin registers a keyboard shortcut in Spyder's shortcuts preferences under the editor context.
93
+ If that shortcut is still empty, it assigns `Ctrl+Alt+C` by default. Repeating it quickly adds the nearest cell marker, then the cursor line number.
94
+ It also registers a keyboard shortcut in the IPython console context and assigns `Ctrl+Alt+K` by default if that shortcut is still empty.
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README-pypi.md
4
+ README.md
5
+ pyproject.toml
6
+ setup.cfg
7
+ setup.py
8
+ assets/spyder_copy_current.png
9
+ spyder_copy_current/__init__.py
10
+ spyder_copy_current/plugin.py
11
+ spyder_copy_current.egg-info/PKG-INFO
12
+ spyder_copy_current.egg-info/SOURCES.txt
13
+ spyder_copy_current.egg-info/dependency_links.txt
14
+ spyder_copy_current.egg-info/entry_points.txt
15
+ spyder_copy_current.egg-info/requires.txt
16
+ spyder_copy_current.egg-info/top_level.txt
17
+ spyder_copy_current/spyder/__init__.py
18
+ spyder_copy_current/spyder/plugin.py
@@ -0,0 +1,2 @@
1
+ [spyder.plugins]
2
+ spyder_copy_current = spyder_copy_current.spyder.plugin:CopyCurrentPlugin
@@ -0,0 +1,3 @@
1
+ jupyter_client<9,>=7.4.9
2
+ spyder<7,>=5.4
3
+ qtpy
@@ -0,0 +1 @@
1
+ spyder_copy_current