Show More
@@ -7,11 +7,11 b' jobs:' | |||||
7 | runs-on: ubuntu-latest |
|
7 | runs-on: ubuntu-latest | |
8 |
|
8 | |||
9 | steps: |
|
9 | steps: | |
10 |
- uses: actions/checkout@v |
|
10 | - uses: actions/checkout@v3 | |
11 |
- name: Set up Python |
|
11 | - name: Set up Python | |
12 |
uses: actions/setup-python@v |
|
12 | uses: actions/setup-python@v4 | |
13 | with: |
|
13 | with: | |
14 |
python-version: 3. |
|
14 | python-version: 3.x | |
15 | - name: Install Graphviz |
|
15 | - name: Install Graphviz | |
16 | run: | |
|
16 | run: | | |
17 | sudo apt-get update |
|
17 | sudo apt-get update |
@@ -21,9 +21,9 b' jobs:' | |||||
21 | python-version: "3.9" |
|
21 | python-version: "3.9" | |
22 |
|
22 | |||
23 | steps: |
|
23 | steps: | |
24 |
- uses: actions/checkout@v |
|
24 | - uses: actions/checkout@v3 | |
25 | - name: Set up Python ${{ matrix.python-version }} |
|
25 | - name: Set up Python ${{ matrix.python-version }} | |
26 |
uses: actions/setup-python@v |
|
26 | uses: actions/setup-python@v4 | |
27 | with: |
|
27 | with: | |
28 | python-version: ${{ matrix.python-version }} |
|
28 | python-version: ${{ matrix.python-version }} | |
29 | - name: Update Python installer |
|
29 | - name: Update Python installer |
@@ -15,9 +15,9 b' jobs:' | |||||
15 | python-version: [3.8] |
|
15 | python-version: [3.8] | |
16 |
|
16 | |||
17 | steps: |
|
17 | steps: | |
18 |
- uses: actions/checkout@v |
|
18 | - uses: actions/checkout@v3 | |
19 | - name: Set up Python ${{ matrix.python-version }} |
|
19 | - name: Set up Python ${{ matrix.python-version }} | |
20 |
uses: actions/setup-python@v |
|
20 | uses: actions/setup-python@v4 | |
21 | with: |
|
21 | with: | |
22 | python-version: ${{ matrix.python-version }} |
|
22 | python-version: ${{ matrix.python-version }} | |
23 | - name: Install dependencies |
|
23 | - name: Install dependencies |
@@ -14,18 +14,14 b' jobs:' | |||||
14 |
|
14 | |||
15 | runs-on: ubuntu-latest |
|
15 | runs-on: ubuntu-latest | |
16 | timeout-minutes: 5 |
|
16 | timeout-minutes: 5 | |
17 | strategy: |
|
|||
18 | matrix: |
|
|||
19 | python-version: [3.8] |
|
|||
20 |
|
||||
21 | steps: |
|
17 | steps: | |
22 |
- uses: actions/checkout@v |
|
18 | - uses: actions/checkout@v3 | |
23 | with: |
|
19 | with: | |
24 | fetch-depth: 0 |
|
20 | fetch-depth: 0 | |
25 |
- name: Set up Python |
|
21 | - name: Set up Python | |
26 |
uses: actions/setup-python@v |
|
22 | uses: actions/setup-python@v4 | |
27 | with: |
|
23 | with: | |
28 |
python-version: |
|
24 | python-version: 3.x | |
29 | - name: Install dependencies |
|
25 | - name: Install dependencies | |
30 | run: | |
|
26 | run: | | |
31 | python -m pip install --upgrade pip |
|
27 | python -m pip install --upgrade pip |
@@ -49,9 +49,9 b' jobs:' | |||||
49 | deps: test |
|
49 | deps: test | |
50 |
|
50 | |||
51 | steps: |
|
51 | steps: | |
52 |
- uses: actions/checkout@v |
|
52 | - uses: actions/checkout@v3 | |
53 | - name: Set up Python ${{ matrix.python-version }} |
|
53 | - name: Set up Python ${{ matrix.python-version }} | |
54 |
uses: actions/setup-python@v |
|
54 | uses: actions/setup-python@v4 | |
55 | with: |
|
55 | with: | |
56 | python-version: ${{ matrix.python-version }} |
|
56 | python-version: ${{ matrix.python-version }} | |
57 | cache: pip |
|
57 | cache: pip |
@@ -906,19 +906,23 b' class Completer(Configurable):' | |||||
906 | matches = [] |
|
906 | matches = [] | |
907 | match_append = matches.append |
|
907 | match_append = matches.append | |
908 | n = len(text) |
|
908 | n = len(text) | |
909 |
for lst in [ |
|
909 | for lst in [ | |
|
910 | keyword.kwlist, | |||
910 |
|
|
911 | builtin_mod.__dict__.keys(), | |
911 |
|
|
912 | list(self.namespace.keys()), | |
912 |
|
|
913 | list(self.global_namespace.keys()), | |
|
914 | ]: | |||
913 | for word in lst: |
|
915 | for word in lst: | |
914 | if word[:n] == text and word != "__builtins__": |
|
916 | if word[:n] == text and word != "__builtins__": | |
915 | match_append(word) |
|
917 | match_append(word) | |
916 |
|
918 | |||
917 | snake_case_re = re.compile(r"[^_]+(_[^_]+)+?\Z") |
|
919 | snake_case_re = re.compile(r"[^_]+(_[^_]+)+?\Z") | |
918 | for lst in [self.namespace.keys(), |
|
920 | for lst in [list(self.namespace.keys()), list(self.global_namespace.keys())]: | |
919 | self.global_namespace.keys()]: |
|
921 | shortened = { | |
920 |
|
|
922 | "_".join([sub[0] for sub in word.split("_")]): word | |
921 |
|
|
923 | for word in lst | |
|
924 | if snake_case_re.match(word) | |||
|
925 | } | |||
922 | for word in shortened.keys(): |
|
926 | for word in shortened.keys(): | |
923 | if word[:n] == text and word != "__builtins__": |
|
927 | if word[:n] == text and word != "__builtins__": | |
924 | match_append(shortened[word]) |
|
928 | match_append(shortened[word]) |
@@ -202,7 +202,6 b' class HistoryAccessor(HistoryAccessorBase):' | |||||
202 | config : :class:`~traitlets.config.loader.Config` |
|
202 | config : :class:`~traitlets.config.loader.Config` | |
203 | Config object. hist_file can also be set through this. |
|
203 | Config object. hist_file can also be set through this. | |
204 | """ |
|
204 | """ | |
205 | # We need a pointer back to the shell for various tasks. |
|
|||
206 | super(HistoryAccessor, self).__init__(**traits) |
|
205 | super(HistoryAccessor, self).__init__(**traits) | |
207 | # defer setting hist_file from kwarg until after init, |
|
206 | # defer setting hist_file from kwarg until after init, | |
208 | # otherwise the default kwarg value would clobber any value |
|
207 | # otherwise the default kwarg value would clobber any value | |
@@ -344,11 +343,6 b' class HistoryAccessor(HistoryAccessorBase):' | |||||
344 | def get_tail(self, n=10, raw=True, output=False, include_latest=False): |
|
343 | def get_tail(self, n=10, raw=True, output=False, include_latest=False): | |
345 | """Get the last n lines from the history database. |
|
344 | """Get the last n lines from the history database. | |
346 |
|
345 | |||
347 | Most recent entry last. |
|
|||
348 |
|
||||
349 | Completion will be reordered so that that the last ones are when |
|
|||
350 | possible from current session. |
|
|||
351 |
|
||||
352 | Parameters |
|
346 | Parameters | |
353 | ---------- |
|
347 | ---------- | |
354 | n : int |
|
348 | n : int | |
@@ -367,31 +361,12 b' class HistoryAccessor(HistoryAccessorBase):' | |||||
367 | self.writeout_cache() |
|
361 | self.writeout_cache() | |
368 | if not include_latest: |
|
362 | if not include_latest: | |
369 | n += 1 |
|
363 | n += 1 | |
370 | # cursor/line/entry |
|
364 | cur = self._run_sql( | |
371 | this_cur = list( |
|
365 | "ORDER BY session DESC, line DESC LIMIT ?", (n,), raw=raw, output=output | |
372 | self._run_sql( |
|
|||
373 | "WHERE session == ? ORDER BY line DESC LIMIT ? ", |
|
|||
374 | (self.session_number, n), |
|
|||
375 | raw=raw, |
|
|||
376 | output=output, |
|
|||
377 | ) |
|
|||
378 | ) |
|
|||
379 | other_cur = list( |
|
|||
380 | self._run_sql( |
|
|||
381 | "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?", |
|
|||
382 | (self.session_number, n), |
|
|||
383 | raw=raw, |
|
|||
384 | output=output, |
|
|||
385 | ) |
|
|||
386 | ) |
|
366 | ) | |
387 |
|
||||
388 | everything = this_cur + other_cur |
|
|||
389 |
|
||||
390 | everything = everything[:n] |
|
|||
391 |
|
||||
392 | if not include_latest: |
|
367 | if not include_latest: | |
393 |
return list( |
|
368 | return reversed(list(cur)[1:]) | |
394 |
return list( |
|
369 | return reversed(list(cur)) | |
395 |
|
370 | |||
396 | @catch_corrupt_db |
|
371 | @catch_corrupt_db | |
397 | def search(self, pattern="*", raw=True, search_raw=True, |
|
372 | def search(self, pattern="*", raw=True, search_raw=True, | |
@@ -560,7 +535,6 b' class HistoryManager(HistoryAccessor):' | |||||
560 | def __init__(self, shell=None, config=None, **traits): |
|
535 | def __init__(self, shell=None, config=None, **traits): | |
561 | """Create a new history manager associated with a shell instance. |
|
536 | """Create a new history manager associated with a shell instance. | |
562 | """ |
|
537 | """ | |
563 | # We need a pointer back to the shell for various tasks. |
|
|||
564 | super(HistoryManager, self).__init__(shell=shell, config=config, |
|
538 | super(HistoryManager, self).__init__(shell=shell, config=config, | |
565 | **traits) |
|
539 | **traits) | |
566 | self.save_flag = threading.Event() |
|
540 | self.save_flag = threading.Event() | |
@@ -656,6 +630,59 b' class HistoryManager(HistoryAccessor):' | |||||
656 |
|
630 | |||
657 | return super(HistoryManager, self).get_session_info(session=session) |
|
631 | return super(HistoryManager, self).get_session_info(session=session) | |
658 |
|
632 | |||
|
633 | @catch_corrupt_db | |||
|
634 | def get_tail(self, n=10, raw=True, output=False, include_latest=False): | |||
|
635 | """Get the last n lines from the history database. | |||
|
636 | ||||
|
637 | Most recent entry last. | |||
|
638 | ||||
|
639 | Completion will be reordered so that that the last ones are when | |||
|
640 | possible from current session. | |||
|
641 | ||||
|
642 | Parameters | |||
|
643 | ---------- | |||
|
644 | n : int | |||
|
645 | The number of lines to get | |||
|
646 | raw, output : bool | |||
|
647 | See :meth:`get_range` | |||
|
648 | include_latest : bool | |||
|
649 | If False (default), n+1 lines are fetched, and the latest one | |||
|
650 | is discarded. This is intended to be used where the function | |||
|
651 | is called by a user command, which it should not return. | |||
|
652 | ||||
|
653 | Returns | |||
|
654 | ------- | |||
|
655 | Tuples as :meth:`get_range` | |||
|
656 | """ | |||
|
657 | self.writeout_cache() | |||
|
658 | if not include_latest: | |||
|
659 | n += 1 | |||
|
660 | # cursor/line/entry | |||
|
661 | this_cur = list( | |||
|
662 | self._run_sql( | |||
|
663 | "WHERE session == ? ORDER BY line DESC LIMIT ? ", | |||
|
664 | (self.session_number, n), | |||
|
665 | raw=raw, | |||
|
666 | output=output, | |||
|
667 | ) | |||
|
668 | ) | |||
|
669 | other_cur = list( | |||
|
670 | self._run_sql( | |||
|
671 | "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?", | |||
|
672 | (self.session_number, n), | |||
|
673 | raw=raw, | |||
|
674 | output=output, | |||
|
675 | ) | |||
|
676 | ) | |||
|
677 | ||||
|
678 | everything = this_cur + other_cur | |||
|
679 | ||||
|
680 | everything = everything[:n] | |||
|
681 | ||||
|
682 | if not include_latest: | |||
|
683 | return list(everything)[:0:-1] | |||
|
684 | return list(everything)[::-1] | |||
|
685 | ||||
659 | def _get_range_session(self, start=1, stop=None, raw=True, output=False): |
|
686 | def _get_range_session(self, start=1, stop=None, raw=True, output=False): | |
660 | """Get input and output history from the current session. Called by |
|
687 | """Get input and output history from the current session. Called by | |
661 | get_range, and takes similar parameters.""" |
|
688 | get_range, and takes similar parameters.""" |
@@ -155,15 +155,17 b' def clipboard_get(self):' | |||||
155 | """ Get text from the clipboard. |
|
155 | """ Get text from the clipboard. | |
156 | """ |
|
156 | """ | |
157 | from ..lib.clipboard import ( |
|
157 | from ..lib.clipboard import ( | |
158 |
osx_clipboard_get, |
|
158 | osx_clipboard_get, | |
159 |
|
|
159 | tkinter_clipboard_get, | |
|
160 | win32_clipboard_get, | |||
|
161 | wayland_clipboard_get, | |||
160 | ) |
|
162 | ) | |
161 | if sys.platform == 'win32': |
|
163 | if sys.platform == 'win32': | |
162 | chain = [win32_clipboard_get, tkinter_clipboard_get] |
|
164 | chain = [win32_clipboard_get, tkinter_clipboard_get] | |
163 | elif sys.platform == 'darwin': |
|
165 | elif sys.platform == 'darwin': | |
164 | chain = [osx_clipboard_get, tkinter_clipboard_get] |
|
166 | chain = [osx_clipboard_get, tkinter_clipboard_get] | |
165 | else: |
|
167 | else: | |
166 | chain = [tkinter_clipboard_get] |
|
168 | chain = [wayland_clipboard_get, tkinter_clipboard_get] | |
167 | dispatcher = CommandChainDispatcher() |
|
169 | dispatcher = CommandChainDispatcher() | |
168 | for func in chain: |
|
170 | for func in chain: | |
169 | dispatcher.add(func) |
|
171 | dispatcher.add(func) |
@@ -8,6 +8,7 b' builtin.' | |||||
8 |
|
8 | |||
9 | import io |
|
9 | import io | |
10 | import os |
|
10 | import os | |
|
11 | import pathlib | |||
11 | import re |
|
12 | import re | |
12 | import sys |
|
13 | import sys | |
13 | from pprint import pformat |
|
14 | from pprint import pformat | |
@@ -409,7 +410,7 b' class OSMagics(Magics):' | |||||
409 | except OSError: |
|
410 | except OSError: | |
410 | print(sys.exc_info()[1]) |
|
411 | print(sys.exc_info()[1]) | |
411 | else: |
|
412 | else: | |
412 |
cwd = |
|
413 | cwd = pathlib.Path.cwd() | |
413 | dhist = self.shell.user_ns['_dh'] |
|
414 | dhist = self.shell.user_ns['_dh'] | |
414 | if oldcwd != cwd: |
|
415 | if oldcwd != cwd: | |
415 | dhist.append(cwd) |
|
416 | dhist.append(cwd) | |
@@ -419,7 +420,7 b' class OSMagics(Magics):' | |||||
419 | os.chdir(self.shell.home_dir) |
|
420 | os.chdir(self.shell.home_dir) | |
420 | if hasattr(self.shell, 'term_title') and self.shell.term_title: |
|
421 | if hasattr(self.shell, 'term_title') and self.shell.term_title: | |
421 | set_term_title(self.shell.term_title_format.format(cwd="~")) |
|
422 | set_term_title(self.shell.term_title_format.format(cwd="~")) | |
422 |
cwd = |
|
423 | cwd = pathlib.Path.cwd() | |
423 | dhist = self.shell.user_ns['_dh'] |
|
424 | dhist = self.shell.user_ns['_dh'] | |
424 |
|
425 | |||
425 | if oldcwd != cwd: |
|
426 | if oldcwd != cwd: |
@@ -16,7 +16,7 b'' | |||||
16 | # release. 'dev' as a _version_extra string means this is a development |
|
16 | # release. 'dev' as a _version_extra string means this is a development | |
17 | # version |
|
17 | # version | |
18 | _version_major = 8 |
|
18 | _version_major = 8 | |
19 |
_version_minor = |
|
19 | _version_minor = 6 | |
20 | _version_patch = 0 |
|
20 | _version_patch = 0 | |
21 | _version_extra = ".dev" |
|
21 | _version_extra = ".dev" | |
22 | # _version_extra = "rc1" |
|
22 | # _version_extra = "rc1" |
@@ -8,6 +8,7 b" with better-isolated tests that don't rely on the global instance in iptest." | |||||
8 | from IPython.core.splitinput import LineInfo |
|
8 | from IPython.core.splitinput import LineInfo | |
9 | from IPython.core.prefilter import AutocallChecker |
|
9 | from IPython.core.prefilter import AutocallChecker | |
10 |
|
10 | |||
|
11 | ||||
11 | def doctest_autocall(): |
|
12 | def doctest_autocall(): | |
12 | """ |
|
13 | """ | |
13 | In [1]: def f1(a,b,c): |
|
14 | In [1]: def f1(a,b,c): |
@@ -28,7 +28,7 b' def test_output_quiet():' | |||||
28 | with AssertNotPrints('2'): |
|
28 | with AssertNotPrints('2'): | |
29 | ip.run_cell('1+1;\n#commented_out_function()', store_history=True) |
|
29 | ip.run_cell('1+1;\n#commented_out_function()', store_history=True) | |
30 |
|
30 | |||
31 | def test_underscore_no_overrite_user(): |
|
31 | def test_underscore_no_overwrite_user(): | |
32 | ip.run_cell('_ = 42', store_history=True) |
|
32 | ip.run_cell('_ = 42', store_history=True) | |
33 | ip.run_cell('1+1', store_history=True) |
|
33 | ip.run_cell('1+1', store_history=True) | |
34 |
|
34 | |||
@@ -41,7 +41,7 b' def test_underscore_no_overrite_user():' | |||||
41 | ip.run_cell('_', store_history=True) |
|
41 | ip.run_cell('_', store_history=True) | |
42 |
|
42 | |||
43 |
|
43 | |||
44 | def test_underscore_no_overrite_builtins(): |
|
44 | def test_underscore_no_overwrite_builtins(): | |
45 | ip.run_cell("import gettext ; gettext.install('foo')", store_history=True) |
|
45 | ip.run_cell("import gettext ; gettext.install('foo')", store_history=True) | |
46 | ip.run_cell('3+3', store_history=True) |
|
46 | ip.run_cell('3+3', store_history=True) | |
47 |
|
47 |
@@ -17,7 +17,7 b' from tempfile import TemporaryDirectory' | |||||
17 | # our own packages |
|
17 | # our own packages | |
18 | from traitlets.config.loader import Config |
|
18 | from traitlets.config.loader import Config | |
19 |
|
19 | |||
20 | from IPython.core.history import HistoryManager, extract_hist_ranges |
|
20 | from IPython.core.history import HistoryAccessor, HistoryManager, extract_hist_ranges | |
21 |
|
21 | |||
22 |
|
22 | |||
23 | def test_proper_default_encoding(): |
|
23 | def test_proper_default_encoding(): | |
@@ -227,3 +227,81 b' def test_histmanager_disabled():' | |||||
227 |
|
227 | |||
228 | # hist_file should not be created |
|
228 | # hist_file should not be created | |
229 | assert hist_file.exists() is False |
|
229 | assert hist_file.exists() is False | |
|
230 | ||||
|
231 | ||||
|
232 | def test_get_tail_session_awareness(): | |||
|
233 | """Test .get_tail() is: | |||
|
234 | - session specific in HistoryManager | |||
|
235 | - session agnostic in HistoryAccessor | |||
|
236 | same for .get_last_session_id() | |||
|
237 | """ | |||
|
238 | ip = get_ipython() | |||
|
239 | with TemporaryDirectory() as tmpdir: | |||
|
240 | tmp_path = Path(tmpdir) | |||
|
241 | hist_file = tmp_path / "history.sqlite" | |||
|
242 | get_source = lambda x: x[2] | |||
|
243 | hm1 = None | |||
|
244 | hm2 = None | |||
|
245 | ha = None | |||
|
246 | try: | |||
|
247 | # hm1 creates a new session and adds history entries, | |||
|
248 | # ha catches up | |||
|
249 | hm1 = HistoryManager(shell=ip, hist_file=hist_file) | |||
|
250 | hm1_last_sid = hm1.get_last_session_id | |||
|
251 | ha = HistoryAccessor(hist_file=hist_file) | |||
|
252 | ha_last_sid = ha.get_last_session_id | |||
|
253 | ||||
|
254 | hist1 = ["a=1", "b=1", "c=1"] | |||
|
255 | for i, h in enumerate(hist1 + [""], start=1): | |||
|
256 | hm1.store_inputs(i, h) | |||
|
257 | assert list(map(get_source, hm1.get_tail())) == hist1 | |||
|
258 | assert list(map(get_source, ha.get_tail())) == hist1 | |||
|
259 | sid1 = hm1_last_sid() | |||
|
260 | assert sid1 is not None | |||
|
261 | assert ha_last_sid() == sid1 | |||
|
262 | ||||
|
263 | # hm2 creates a new session and adds entries, | |||
|
264 | # ha catches up | |||
|
265 | hm2 = HistoryManager(shell=ip, hist_file=hist_file) | |||
|
266 | hm2_last_sid = hm2.get_last_session_id | |||
|
267 | ||||
|
268 | hist2 = ["a=2", "b=2", "c=2"] | |||
|
269 | for i, h in enumerate(hist2 + [""], start=1): | |||
|
270 | hm2.store_inputs(i, h) | |||
|
271 | tail = hm2.get_tail(n=3) | |||
|
272 | assert list(map(get_source, tail)) == hist2 | |||
|
273 | tail = ha.get_tail(n=3) | |||
|
274 | assert list(map(get_source, tail)) == hist2 | |||
|
275 | sid2 = hm2_last_sid() | |||
|
276 | assert sid2 is not None | |||
|
277 | assert ha_last_sid() == sid2 | |||
|
278 | assert sid2 != sid1 | |||
|
279 | ||||
|
280 | # but hm1 still maintains its point of reference | |||
|
281 | # and adding more entries to it doesn't change others | |||
|
282 | # immediate perspective | |||
|
283 | assert hm1_last_sid() == sid1 | |||
|
284 | tail = hm1.get_tail(n=3) | |||
|
285 | assert list(map(get_source, tail)) == hist1 | |||
|
286 | ||||
|
287 | hist3 = ["a=3", "b=3", "c=3"] | |||
|
288 | for i, h in enumerate(hist3 + [""], start=5): | |||
|
289 | hm1.store_inputs(i, h) | |||
|
290 | tail = hm1.get_tail(n=7) | |||
|
291 | assert list(map(get_source, tail)) == hist1 + [""] + hist3 | |||
|
292 | tail = hm2.get_tail(n=3) | |||
|
293 | assert list(map(get_source, tail)) == hist2 | |||
|
294 | tail = ha.get_tail(n=3) | |||
|
295 | assert list(map(get_source, tail)) == hist2 | |||
|
296 | assert hm1_last_sid() == sid1 | |||
|
297 | assert hm2_last_sid() == sid2 | |||
|
298 | assert ha_last_sid() == sid2 | |||
|
299 | finally: | |||
|
300 | if hm1: | |||
|
301 | hm1.save_thread.stop() | |||
|
302 | hm1.db.close() | |||
|
303 | if hm2: | |||
|
304 | hm2.save_thread.stop() | |||
|
305 | hm2.db.close() | |||
|
306 | if ha: | |||
|
307 | ha.db.close() |
@@ -32,6 +32,7 b' tests.append(("P\xc3\xa9rez Fernando", ("", "", "P\xc3\xa9rez", "Fernando")))' | |||||
32 | def test_split_user_input(): |
|
32 | def test_split_user_input(): | |
33 | return tt.check_pairs(split_user_input, tests) |
|
33 | return tt.check_pairs(split_user_input, tests) | |
34 |
|
34 | |||
|
35 | ||||
35 | def test_LineInfo(): |
|
36 | def test_LineInfo(): | |
36 | """Simple test for LineInfo construction and str()""" |
|
37 | """Simple test for LineInfo construction and str()""" | |
37 | linfo = LineInfo(" %cd /home") |
|
38 | linfo = LineInfo(" %cd /home") |
@@ -300,7 +300,7 b' def update_instances(old, new):' | |||||
300 |
|
300 | |||
301 | for ref in refs: |
|
301 | for ref in refs: | |
302 | if type(ref) is old: |
|
302 | if type(ref) is old: | |
303 |
ref |
|
303 | object.__setattr__(ref, "__class__", new) | |
304 |
|
304 | |||
305 |
|
305 | |||
306 | def update_class(old, new): |
|
306 | def update_class(old, new): |
@@ -1,14 +1,16 b'' | |||||
1 | """ Utilities for accessing the platform's clipboard. |
|
1 | """ Utilities for accessing the platform's clipboard. | |
2 | """ |
|
2 | """ | |
3 |
|
3 | import os | ||
4 | import subprocess |
|
4 | import subprocess | |
5 |
|
5 | |||
6 | from IPython.core.error import TryNext |
|
6 | from IPython.core.error import TryNext | |
7 | import IPython.utils.py3compat as py3compat |
|
7 | import IPython.utils.py3compat as py3compat | |
8 |
|
8 | |||
|
9 | ||||
9 | class ClipboardEmpty(ValueError): |
|
10 | class ClipboardEmpty(ValueError): | |
10 | pass |
|
11 | pass | |
11 |
|
12 | |||
|
13 | ||||
12 | def win32_clipboard_get(): |
|
14 | def win32_clipboard_get(): | |
13 | """ Get the current clipboard's text on Windows. |
|
15 | """ Get the current clipboard's text on Windows. | |
14 |
|
16 | |||
@@ -32,6 +34,7 b' def win32_clipboard_get():' | |||||
32 | win32clipboard.CloseClipboard() |
|
34 | win32clipboard.CloseClipboard() | |
33 | return text |
|
35 | return text | |
34 |
|
36 | |||
|
37 | ||||
35 | def osx_clipboard_get() -> str: |
|
38 | def osx_clipboard_get() -> str: | |
36 | """ Get the clipboard's text on OS X. |
|
39 | """ Get the clipboard's text on OS X. | |
37 | """ |
|
40 | """ | |
@@ -43,6 +46,7 b' def osx_clipboard_get() -> str:' | |||||
43 | text = py3compat.decode(bytes_) |
|
46 | text = py3compat.decode(bytes_) | |
44 | return text |
|
47 | return text | |
45 |
|
48 | |||
|
49 | ||||
46 | def tkinter_clipboard_get(): |
|
50 | def tkinter_clipboard_get(): | |
47 | """ Get the clipboard's text using Tkinter. |
|
51 | """ Get the clipboard's text using Tkinter. | |
48 |
|
52 | |||
@@ -67,3 +71,31 b' def tkinter_clipboard_get():' | |||||
67 | return text |
|
71 | return text | |
68 |
|
72 | |||
69 |
|
73 | |||
|
74 | def wayland_clipboard_get(): | |||
|
75 | """Get the clipboard's text under Wayland using wl-paste command. | |||
|
76 | ||||
|
77 | This requires Wayland and wl-clipboard installed and running. | |||
|
78 | """ | |||
|
79 | if os.environ.get("XDG_SESSION_TYPE") != "wayland": | |||
|
80 | raise TryNext("wayland is not detected") | |||
|
81 | ||||
|
82 | try: | |||
|
83 | with subprocess.Popen(["wl-paste"], stdout=subprocess.PIPE) as p: | |||
|
84 | raw, err = p.communicate() | |||
|
85 | if p.wait(): | |||
|
86 | raise TryNext(err) | |||
|
87 | except FileNotFoundError as e: | |||
|
88 | raise TryNext( | |||
|
89 | "Getting text from the clipboard under Wayland requires the wl-clipboard " | |||
|
90 | "extension: https://github.com/bugaevc/wl-clipboard" | |||
|
91 | ) from e | |||
|
92 | ||||
|
93 | if not raw: | |||
|
94 | raise ClipboardEmpty | |||
|
95 | ||||
|
96 | try: | |||
|
97 | text = py3compat.decode(raw) | |||
|
98 | except UnicodeDecodeError as e: | |||
|
99 | raise ClipboardEmpty from e | |||
|
100 | ||||
|
101 | return text |
@@ -144,19 +144,30 b" def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):" | |||||
144 | find_cmd('dvipng') |
|
144 | find_cmd('dvipng') | |
145 | except FindCmdError: |
|
145 | except FindCmdError: | |
146 | return None |
|
146 | return None | |
|
147 | ||||
|
148 | startupinfo = None | |||
|
149 | if os.name == "nt": | |||
|
150 | # prevent popup-windows | |||
|
151 | startupinfo = subprocess.STARTUPINFO() | |||
|
152 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW | |||
|
153 | ||||
147 | try: |
|
154 | try: | |
148 | workdir = Path(tempfile.mkdtemp()) |
|
155 | workdir = Path(tempfile.mkdtemp()) | |
149 |
tmpfile = |
|
156 | tmpfile = "tmp.tex" | |
150 |
dvifile = |
|
157 | dvifile = "tmp.dvi" | |
151 |
outfile = |
|
158 | outfile = "tmp.png" | |
152 |
|
159 | |||
153 | with tmpfile.open("w", encoding="utf8") as f: |
|
160 | with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f: | |
154 | f.writelines(genelatex(s, wrap)) |
|
161 | f.writelines(genelatex(s, wrap)) | |
155 |
|
162 | |||
156 | with open(os.devnull, 'wb') as devnull: |
|
163 | with open(os.devnull, 'wb') as devnull: | |
157 | subprocess.check_call( |
|
164 | subprocess.check_call( | |
158 | ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile], |
|
165 | ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile], | |
159 |
cwd=workdir, |
|
166 | cwd=workdir, | |
|
167 | stdout=devnull, | |||
|
168 | stderr=devnull, | |||
|
169 | startupinfo=startupinfo, | |||
|
170 | ) | |||
160 |
|
171 | |||
161 | resolution = round(150*scale) |
|
172 | resolution = round(150*scale) | |
162 | subprocess.check_call( |
|
173 | subprocess.check_call( | |
@@ -179,9 +190,10 b" def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):" | |||||
179 | cwd=workdir, |
|
190 | cwd=workdir, | |
180 | stdout=devnull, |
|
191 | stdout=devnull, | |
181 | stderr=devnull, |
|
192 | stderr=devnull, | |
|
193 | startupinfo=startupinfo, | |||
182 | ) |
|
194 | ) | |
183 |
|
195 | |||
184 | with outfile.open("rb") as f: |
|
196 | with workdir.joinpath(outfile).open("rb") as f: | |
185 | return f.read() |
|
197 | return f.read() | |
186 | except subprocess.CalledProcessError: |
|
198 | except subprocess.CalledProcessError: | |
187 | return None |
|
199 | return None |
@@ -908,6 +908,8 b' def _deque_pprint(obj, p, cycle):' | |||||
908 | cls_ctor = CallExpression.factory(obj.__class__.__name__) |
|
908 | cls_ctor = CallExpression.factory(obj.__class__.__name__) | |
909 | if cycle: |
|
909 | if cycle: | |
910 | p.pretty(cls_ctor(RawText("..."))) |
|
910 | p.pretty(cls_ctor(RawText("..."))) | |
|
911 | elif obj.maxlen is not None: | |||
|
912 | p.pretty(cls_ctor(list(obj), maxlen=obj.maxlen)) | |||
911 | else: |
|
913 | else: | |
912 | p.pretty(cls_ctor(list(obj))) |
|
914 | p.pretty(cls_ctor(list(obj))) | |
913 |
|
915 |
@@ -2,6 +2,7 b' from IPython.core.error import TryNext' | |||||
2 | from IPython.lib.clipboard import ClipboardEmpty |
|
2 | from IPython.lib.clipboard import ClipboardEmpty | |
3 | from IPython.testing.decorators import skip_if_no_x11 |
|
3 | from IPython.testing.decorators import skip_if_no_x11 | |
4 |
|
4 | |||
|
5 | ||||
5 | @skip_if_no_x11 |
|
6 | @skip_if_no_x11 | |
6 | def test_clipboard_get(): |
|
7 | def test_clipboard_get(): | |
7 | # Smoketest for clipboard access - we can't easily guarantee that the |
|
8 | # Smoketest for clipboard access - we can't easily guarantee that the |
@@ -91,7 +91,12 b' def get_default_editor():' | |||||
91 | # - no isatty method |
|
91 | # - no isatty method | |
92 | for _name in ('stdin', 'stdout', 'stderr'): |
|
92 | for _name in ('stdin', 'stdout', 'stderr'): | |
93 | _stream = getattr(sys, _name) |
|
93 | _stream = getattr(sys, _name) | |
94 | if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty(): |
|
94 | try: | |
|
95 | if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty(): | |||
|
96 | _is_tty = False | |||
|
97 | break | |||
|
98 | except ValueError: | |||
|
99 | # stream is closed | |||
95 | _is_tty = False |
|
100 | _is_tty = False | |
96 | break |
|
101 | break | |
97 | else: |
|
102 | else: |
@@ -48,10 +48,17 b' def _elide_point(string:str, *, min_elide=30)->str:' | |||||
48 | file_parts.pop() |
|
48 | file_parts.pop() | |
49 |
|
49 | |||
50 | if len(object_parts) > 3: |
|
50 | if len(object_parts) > 3: | |
51 | return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][0], object_parts[-2][-1], object_parts[-1]) |
|
51 | return "{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}".format( | |
|
52 | object_parts[0], | |||
|
53 | object_parts[1][:1], | |||
|
54 | object_parts[-2][-1:], | |||
|
55 | object_parts[-1], | |||
|
56 | ) | |||
52 |
|
57 | |||
53 | elif len(file_parts) > 3: |
|
58 | elif len(file_parts) > 3: | |
54 |
return ( |
|
59 | return ("{}" + os.sep + "{}\N{HORIZONTAL ELLIPSIS}{}" + os.sep + "{}").format( | |
|
60 | file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1] | |||
|
61 | ) | |||
55 |
|
62 | |||
56 | return string |
|
63 | return string | |
57 |
|
64 |
@@ -140,6 +140,18 b' def create_ipython_shortcuts(shell):' | |||||
140 | _following_text_cache[pattern] = condition |
|
140 | _following_text_cache[pattern] = condition | |
141 | return condition |
|
141 | return condition | |
142 |
|
142 | |||
|
143 | @Condition | |||
|
144 | def not_inside_unclosed_string(): | |||
|
145 | app = get_app() | |||
|
146 | s = app.current_buffer.document.text_before_cursor | |||
|
147 | # remove escaped quotes | |||
|
148 | s = s.replace('\\"', "").replace("\\'", "") | |||
|
149 | # remove triple-quoted string literals | |||
|
150 | s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s) | |||
|
151 | # remove single-quoted string literals | |||
|
152 | s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s) | |||
|
153 | return not ('"' in s or "'" in s) | |||
|
154 | ||||
143 | # auto match |
|
155 | # auto match | |
144 | @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) |
|
156 | @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) | |
145 | def _(event): |
|
157 | def _(event): | |
@@ -160,7 +172,7 b' def create_ipython_shortcuts(shell):' | |||||
160 | '"', |
|
172 | '"', | |
161 | filter=focused_insert |
|
173 | filter=focused_insert | |
162 | & auto_match |
|
174 | & auto_match | |
163 | & preceding_text(r'^([^"]+|"[^"]*")*$') |
|
175 | & not_inside_unclosed_string | |
164 | & following_text(r"[,)}\]]|$"), |
|
176 | & following_text(r"[,)}\]]|$"), | |
165 | ) |
|
177 | ) | |
166 | def _(event): |
|
178 | def _(event): | |
@@ -171,13 +183,35 b' def create_ipython_shortcuts(shell):' | |||||
171 | "'", |
|
183 | "'", | |
172 | filter=focused_insert |
|
184 | filter=focused_insert | |
173 | & auto_match |
|
185 | & auto_match | |
174 | & preceding_text(r"^([^']+|'[^']*')*$") |
|
186 | & not_inside_unclosed_string | |
175 | & following_text(r"[,)}\]]|$"), |
|
187 | & following_text(r"[,)}\]]|$"), | |
176 | ) |
|
188 | ) | |
177 | def _(event): |
|
189 | def _(event): | |
178 | event.current_buffer.insert_text("''") |
|
190 | event.current_buffer.insert_text("''") | |
179 | event.current_buffer.cursor_left() |
|
191 | event.current_buffer.cursor_left() | |
180 |
|
192 | |||
|
193 | @kb.add( | |||
|
194 | '"', | |||
|
195 | filter=focused_insert | |||
|
196 | & auto_match | |||
|
197 | & not_inside_unclosed_string | |||
|
198 | & preceding_text(r'^.*""$'), | |||
|
199 | ) | |||
|
200 | def _(event): | |||
|
201 | event.current_buffer.insert_text('""""') | |||
|
202 | event.current_buffer.cursor_left(3) | |||
|
203 | ||||
|
204 | @kb.add( | |||
|
205 | "'", | |||
|
206 | filter=focused_insert | |||
|
207 | & auto_match | |||
|
208 | & not_inside_unclosed_string | |||
|
209 | & preceding_text(r"^.*''$"), | |||
|
210 | ) | |||
|
211 | def _(event): | |||
|
212 | event.current_buffer.insert_text("''''") | |||
|
213 | event.current_buffer.cursor_left(3) | |||
|
214 | ||||
181 | # raw string |
|
215 | # raw string | |
182 | @kb.add( |
|
216 | @kb.add( | |
183 | "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") |
|
217 | "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") |
@@ -7,6 +7,7 b' import warnings' | |||||
7 | # Copyright (c) IPython Development Team. |
|
7 | # Copyright (c) IPython Development Team. | |
8 | # Distributed under the terms of the Modified BSD License. |
|
8 | # Distributed under the terms of the Modified BSD License. | |
9 |
|
9 | |||
|
10 | ||||
10 | class preserve_keys(object): |
|
11 | class preserve_keys(object): | |
11 | """Preserve a set of keys in a dictionary. |
|
12 | """Preserve a set of keys in a dictionary. | |
12 |
|
13 |
@@ -1,4 +1,3 b'' | |||||
1 |
|
||||
2 |
|
|
1 | from warnings import warn | |
3 |
|
2 | |||
4 | warn("IPython.utils.eventful has moved to traitlets.eventful", stacklevel=2) |
|
3 | warn("IPython.utils.eventful has moved to traitlets.eventful", stacklevel=2) |
@@ -1,4 +1,3 b'' | |||||
1 |
|
||||
2 |
|
|
1 | from warnings import warn | |
3 |
|
2 | |||
4 | warn("IPython.utils.log has moved to traitlets.log", stacklevel=2) |
|
3 | warn("IPython.utils.log has moved to traitlets.log", stacklevel=2) |
@@ -83,12 +83,13 b' def get_py_filename(name):' | |||||
83 | """ |
|
83 | """ | |
84 |
|
84 | |||
85 | name = os.path.expanduser(name) |
|
85 | name = os.path.expanduser(name) | |
86 | if not os.path.isfile(name) and not name.endswith('.py'): |
|
|||
87 | name += '.py' |
|
|||
88 | if os.path.isfile(name): |
|
86 | if os.path.isfile(name): | |
89 | return name |
|
87 | return name | |
90 | else: |
|
88 | if not name.endswith(".py"): | |
91 | raise IOError('File `%r` not found.' % name) |
|
89 | py_name = name + ".py" | |
|
90 | if os.path.isfile(py_name): | |||
|
91 | return py_name | |||
|
92 | raise IOError("File `%r` not found." % name) | |||
92 |
|
93 | |||
93 |
|
94 | |||
94 | def filefind(filename: str, path_dirs=None) -> str: |
|
95 | def filefind(filename: str, path_dirs=None) -> str: |
@@ -48,6 +48,7 b' class TemporaryWorkingDirectory(TemporaryDirectory):' | |||||
48 | with TemporaryWorkingDirectory() as tmpdir: |
|
48 | with TemporaryWorkingDirectory() as tmpdir: | |
49 | ... |
|
49 | ... | |
50 | """ |
|
50 | """ | |
|
51 | ||||
51 | def __enter__(self): |
|
52 | def __enter__(self): | |
52 | self.old_wd = Path.cwd() |
|
53 | self.old_wd = Path.cwd() | |
53 | _os.chdir(self.name) |
|
54 | _os.chdir(self.name) |
@@ -25,6 +25,7 b' Need to be updated:' | |||||
25 |
|
25 | |||
26 |
|
26 | |||
27 |
|
27 | |||
|
28 | ||||
28 | .. DO NOT EDIT THIS LINE BEFORE RELEASE. FEATURE INSERTION POINT. |
|
29 | .. DO NOT EDIT THIS LINE BEFORE RELEASE. FEATURE INSERTION POINT. | |
29 |
|
30 | |||
30 | Backwards incompatible changes |
|
31 | Backwards incompatible changes |
@@ -2,6 +2,122 b'' | |||||
2 | 8.x Series |
|
2 | 8.x Series | |
3 | ============ |
|
3 | ============ | |
4 |
|
4 | |||
|
5 | .. _version 8.5.0: | |||
|
6 | ||||
|
7 | IPython 8.5.0 | |||
|
8 | ------------- | |||
|
9 | ||||
|
10 | First release since a couple of month due to various reasons and timing preventing | |||
|
11 | me for sticking to the usual monthly release the last Friday of each month. This | |||
|
12 | is of non negligible size as it has more than two dozen PRs with various fixes | |||
|
13 | an bug fixes. | |||
|
14 | ||||
|
15 | Many thanks to everybody who contributed PRs for your patience in review and | |||
|
16 | merges. | |||
|
17 | ||||
|
18 | Here is a non exhaustive list of changes that have been implemented for IPython | |||
|
19 | 8.5.0. As usual you can find the full list of issues and PRs tagged with `the | |||
|
20 | 8.5 milestone | |||
|
21 | <https://github.com/ipython/ipython/pulls?q=is%3Aclosed+milestone%3A8.5+>`__. | |||
|
22 | ||||
|
23 | - Added shortcut for accepting auto suggestion. The End key shortcut for | |||
|
24 | accepting auto-suggestion This binding works in Vi mode too, provided | |||
|
25 | ``TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode`` is set to be | |||
|
26 | ``True`` :ghpull:`13566`. | |||
|
27 | ||||
|
28 | - No popup in window for latex generation w hen generating latex (e.g. via | |||
|
29 | `_latex_repr_`) no popup window is shows under Windows. :ghpull:`13679` | |||
|
30 | ||||
|
31 | - Fixed error raised when attempting to tab-complete an input string with | |||
|
32 | consecutive periods or forward slashes (such as "file:///var/log/..."). | |||
|
33 | :ghpull:`13675` | |||
|
34 | ||||
|
35 | - Relative filenames in Latex rendering : | |||
|
36 | The `latex_to_png_dvipng` command internally generates input and output file | |||
|
37 | arguments to `latex` and `dvipis`. These arguments are now generated as | |||
|
38 | relative files to the current working directory instead of absolute file | |||
|
39 | paths. This solves a problem where the current working directory contains | |||
|
40 | characters that are not handled properly by `latex` and `dvips`. There are | |||
|
41 | no changes to the user API. :ghpull:`13680` | |||
|
42 | ||||
|
43 | - Stripping decorators bug: Fixed bug which meant that ipython code blocks in | |||
|
44 | restructured text documents executed with the ipython-sphinx extension | |||
|
45 | skipped any lines of code containing python decorators. :ghpull:`13612` | |||
|
46 | ||||
|
47 | - Allow some modules with frozen dataclasses to be reloaded. :ghpull:`13732` | |||
|
48 | - Fix paste magic on wayland. :ghpull:`13671` | |||
|
49 | - show maxlen in deque's repr. :ghpull:`13648` | |||
|
50 | ||||
|
51 | Restore line numbers for Input | |||
|
52 | ------------------------------ | |||
|
53 | ||||
|
54 | Line number information in tracebacks from input are restored. | |||
|
55 | Line numbers from input were removed during the transition to v8 enhanced traceback reporting. | |||
|
56 | ||||
|
57 | So, instead of:: | |||
|
58 | ||||
|
59 | --------------------------------------------------------------------------- | |||
|
60 | ZeroDivisionError Traceback (most recent call last) | |||
|
61 | Input In [3], in <cell line: 1>() | |||
|
62 | ----> 1 myfunc(2) | |||
|
63 | ||||
|
64 | Input In [2], in myfunc(z) | |||
|
65 | 1 def myfunc(z): | |||
|
66 | ----> 2 foo.boo(z-1) | |||
|
67 | ||||
|
68 | File ~/code/python/ipython/foo.py:3, in boo(x) | |||
|
69 | 2 def boo(x): | |||
|
70 | ----> 3 return 1/(1-x) | |||
|
71 | ||||
|
72 | ZeroDivisionError: division by zero | |||
|
73 | ||||
|
74 | The error traceback now looks like:: | |||
|
75 | ||||
|
76 | --------------------------------------------------------------------------- | |||
|
77 | ZeroDivisionError Traceback (most recent call last) | |||
|
78 | Cell In [3], line 1 | |||
|
79 | ----> 1 myfunc(2) | |||
|
80 | ||||
|
81 | Cell In [2], line 2, in myfunc(z) | |||
|
82 | 1 def myfunc(z): | |||
|
83 | ----> 2 foo.boo(z-1) | |||
|
84 | ||||
|
85 | File ~/code/python/ipython/foo.py:3, in boo(x) | |||
|
86 | 2 def boo(x): | |||
|
87 | ----> 3 return 1/(1-x) | |||
|
88 | ||||
|
89 | ZeroDivisionError: division by zero | |||
|
90 | ||||
|
91 | or, with xmode=Plain:: | |||
|
92 | ||||
|
93 | Traceback (most recent call last): | |||
|
94 | Cell In [12], line 1 | |||
|
95 | myfunc(2) | |||
|
96 | Cell In [6], line 2 in myfunc | |||
|
97 | foo.boo(z-1) | |||
|
98 | File ~/code/python/ipython/foo.py:3 in boo | |||
|
99 | return 1/(1-x) | |||
|
100 | ZeroDivisionError: division by zero | |||
|
101 | ||||
|
102 | :ghpull:`13560` | |||
|
103 | ||||
|
104 | New setting to silence warning if working inside a virtual environment | |||
|
105 | ---------------------------------------------------------------------- | |||
|
106 | ||||
|
107 | Previously, when starting IPython in a virtual environment without IPython installed (so IPython from the global environment is used), the following warning was printed: | |||
|
108 | ||||
|
109 | Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. | |||
|
110 | ||||
|
111 | This warning can be permanently silenced by setting ``c.InteractiveShell.warn_venv`` to ``False`` (the default is ``True``). | |||
|
112 | ||||
|
113 | :ghpull:`13706` | |||
|
114 | ||||
|
115 | ------- | |||
|
116 | ||||
|
117 | Thanks to the `D. E. Shaw group <https://deshaw.com/>`__ for sponsoring | |||
|
118 | work on IPython and related libraries. | |||
|
119 | ||||
|
120 | ||||
5 | .. _version 8.4.0: |
|
121 | .. _version 8.4.0: | |
6 |
|
122 | |||
7 | IPython 8.4.0 |
|
123 | IPython 8.4.0 |
@@ -39,7 +39,6 b' install_requires =' | |||||
39 | pickleshare |
|
39 | pickleshare | |
40 | prompt_toolkit>3.0.1,<3.1.0 |
|
40 | prompt_toolkit>3.0.1,<3.1.0 | |
41 | pygments>=2.4.0 |
|
41 | pygments>=2.4.0 | |
42 | setuptools>=18.5 |
|
|||
43 | stack_data |
|
42 | stack_data | |
44 | traitlets>=5 |
|
43 | traitlets>=5 | |
45 |
|
44 |
@@ -80,7 +80,7 b' def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", p' | |||||
80 | if pulls: |
|
80 | if pulls: | |
81 | filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ] |
|
81 | filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ] | |
82 | # filter out PRs not against main (backports) |
|
82 | # filter out PRs not against main (backports) | |
83 |
filtered = [ |
|
83 | filtered = [i for i in filtered if i["base"]["ref"] == "main"] | |
84 | else: |
|
84 | else: | |
85 | filtered = [ i for i in filtered if not is_pull_request(i) ] |
|
85 | filtered = [ i for i in filtered if not is_pull_request(i) ] | |
86 |
|
86 |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now