diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..d1fed9f3d5f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + actions: + patterns: + - "*" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f18fb39392f..5513b16930b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x - name: Install Graphviz @@ -34,6 +34,6 @@ jobs: run: | coverage combine `find . -name .coverage\*` && coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 with: name: Docs diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 00f19d36777..3340c0182b2 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -25,9 +25,9 @@ jobs: python-version: "3.10" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Update Python installer @@ -53,19 +53,21 @@ jobs: pytest - name: Install sagemath-repl run: | - cd .. - git clone --depth 1 https://github.com/sagemath/sage - cd sage - # We cloned it for the tests, but for simplicity we install the - # wheels from PyPI. - # (Avoid 10.3b6 because of https://github.com/sagemath/sage/pull/37178) - pip install --pre sagemath-repl sagemath-environment - # Install optionals that make more tests pass - pip install pillow - pip install --pre sagemath-categories - cd .. + # Sept 2024, sage has been failing for a while, + # Skipping. + # cd .. + # git clone --depth 1 https://github.com/sagemath/sage + # cd sage + # # We cloned it for the tests, but for simplicity we install the + # # wheels from PyPI. + # # (Avoid 10.3b6 because of https://github.com/sagemath/sage/pull/37178) + # pip install --pre sagemath-repl sagemath-environment + # # Install optionals that make more tests pass + # pip install pillow + # pip install --pre sagemath-categories + # cd .. - name: Test sagemath-repl run: | - cd ../sage/ - # From https://github.com/sagemath/sage/blob/develop/pkgs/sagemath-repl/tox.ini - sage-runtests -p --environment=sage.all__sagemath_repl --baseline-stats-path=pkgs/sagemath-repl/known-test-failures.json --initial --optional=sage src/sage/repl src/sage/doctest src/sage/misc/sage_input.py src/sage/misc/sage_eval.py + # cd ../sage/ + # # From https://github.com/sagemath/sage/blob/develop/pkgs/sagemath-repl/tox.ini + # sage-runtests -p --environment=sage.all__sagemath_repl --baseline-stats-path=pkgs/sagemath-repl/known-test-failures.json --initial --optional=sage src/sage/repl src/sage/doctest src/sage/misc/sage_input.py src/sage/misc/sage_eval.py diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index c65682efb1d..d2bffdb607e 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -18,9 +18,9 @@ jobs: python-version: ["3.x"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/nightly-wheel-build.yml b/.github/workflows/nightly-wheel-build.yml index 64a41e32eda..6f0eaddca53 100644 --- a/.github/workflows/nightly-wheel-build.yml +++ b/.github/workflows/nightly-wheel-build.yml @@ -13,9 +13,9 @@ jobs: if: github.event_name != 'pull_request' && (github.event_name != 'schedule' || github.repository_owner == 'ipython') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" cache: pip diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e65f2e5b3a2..786d00b9509 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -18,11 +18,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 925754b383a..3543bdbe2da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,9 +56,9 @@ jobs: want-latest-entry-point-code: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -72,14 +72,13 @@ jobs: run: | python -m pip install --only-binary ':all:' --upgrade pip setuptools wheel build python -m pip install --only-binary ':all:' --no-binary curio --upgrade -e .[${{ matrix.deps }}] - python -m pip install --only-binary ':all:' --upgrade check-manifest pytest-cov pytest-json-report 'pytest<8' + python -m pip install --only-binary ':all:' --upgrade check-manifest pytest-cov 'pytest<8' - name: Install and update Python dependencies (dev?) if: ${{ contains( matrix.python-version, 'dev' ) }} run: | python -m pip install --pre --upgrade pip setuptools wheel build python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --no-binary curio --upgrade -e .[${{ matrix.deps }}] - python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --upgrade check-manifest pytest-cov pytest-json-report - python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple git+https://github.com/alexmojaki/executing.git@3.13 + python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --upgrade check-manifest pytest-cov - name: Try building with Python build if: runner.os != 'Windows' # setup.py does not support sdist on Windows run: | @@ -102,15 +101,9 @@ jobs: env: COLUMNS: 120 run: | - pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }} --json-report --json-report-file=./report-${{ matrix.python-version }}-${{runner.os}}.json --maxfail=15 - - uses: actions/upload-artifact@v3 - with: - name: upload pytest timing reports as json - path: | - ./report-*.json - + pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }} --maxfail=15 - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: name: Test files: /home/runner/work/ipython/ipython/coverage.xml diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 4eef2b55880..440953d0690 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -898,7 +898,7 @@ def rectify_completions(text: str, completions: _IC, *, _debug: bool = False) -> new_text = text[new_start:c.start] + c.text + text[c.end:new_end] if c._origin == 'jedi': seen_jedi.add(new_text) - elif c._origin == 'IPCompleter.python_matches': + elif c._origin == "IPCompleter.python_matcher": seen_python_matches.add(new_text) yield Completion(new_start, new_end, new_text, type=c.type, _origin=c._origin, signature=c.signature) diff = seen_python_matches.difference(seen_jedi) @@ -1139,15 +1139,18 @@ def attr_matches(self, text): with a __getattr__ hook is evaluated. """ + return self._attr_matches(text)[0] + + def _attr_matches(self, text, include_prefix=True) -> Tuple[Sequence[str], str]: m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) if not m2: - return [] + return [], "" expr, attr = m2.group(1, 2) obj = self._evaluate_expr(expr) if obj is not_found: - return [] + return [], "" if self.limit_to__all__ and hasattr(obj, '__all__'): words = get__all__entries(obj) @@ -1170,28 +1173,36 @@ def attr_matches(self, text): # reconciliator would know that we intend to append to rather than # replace the input text; this requires refactoring to return range # which ought to be replaced (as does jedi). - tokens = _parse_tokens(expr) - rev_tokens = reversed(tokens) - skip_over = {tokenize.ENDMARKER, tokenize.NEWLINE} - name_turn = True - - parts = [] - for token in rev_tokens: - if token.type in skip_over: - continue - if token.type == tokenize.NAME and name_turn: - parts.append(token.string) - name_turn = False - elif token.type == tokenize.OP and token.string == "." and not name_turn: - parts.append(token.string) - name_turn = True - else: - # short-circuit if not empty nor name token - break + if include_prefix: + tokens = _parse_tokens(expr) + rev_tokens = reversed(tokens) + skip_over = {tokenize.ENDMARKER, tokenize.NEWLINE} + name_turn = True + + parts = [] + for token in rev_tokens: + if token.type in skip_over: + continue + if token.type == tokenize.NAME and name_turn: + parts.append(token.string) + name_turn = False + elif ( + token.type == tokenize.OP and token.string == "." and not name_turn + ): + parts.append(token.string) + name_turn = True + else: + # short-circuit if not empty nor name token + break - prefix_after_space = "".join(reversed(parts)) + prefix_after_space = "".join(reversed(parts)) + else: + prefix_after_space = "" - return ["%s.%s" % (prefix_after_space, w) for w in words if w[:n] == attr] + return ( + ["%s.%s" % (prefix_after_space, w) for w in words if w[:n] == attr], + "." + attr, + ) def _evaluate_expr(self, expr): obj = not_found @@ -1973,9 +1984,8 @@ def matchers(self) -> List[Matcher]: *self.magic_arg_matchers, self.custom_completer_matcher, self.dict_key_matcher, - # TODO: convert python_matches to v2 API self.magic_matcher, - self.python_matches, + self.python_matcher, self.file_matcher, self.python_func_kw_matcher, ] @@ -2316,9 +2326,42 @@ def _jedi_matches( else: return iter([]) + @context_matcher() + def python_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Match attributes or global python names""" + text = context.line_with_cursor + if "." in text: + try: + matches, fragment = self._attr_matches(text, include_prefix=False) + if text.endswith(".") and self.omit__names: + if self.omit__names == 1: + # true if txt is _not_ a __ name, false otherwise: + no__name = lambda txt: re.match(r".*\.__.*?__", txt) is None + else: + # true if txt is _not_ a _ name, false otherwise: + no__name = ( + lambda txt: re.match(r"\._.*?", txt[txt.rindex(".") :]) + is None + ) + matches = filter(no__name, matches) + return _convert_matcher_v1_result_to_v2( + matches, type="attribute", fragment=fragment + ) + except NameError: + # catches . + matches = [] + return _convert_matcher_v1_result_to_v2(matches, type="attribute") + else: + matches = self.global_matches(context.token) + # TODO: maybe distinguish between functions, modules and just "variables" + return _convert_matcher_v1_result_to_v2(matches, type="variable") + @completion_matcher(api_version=1) def python_matches(self, text: str) -> Iterable[str]: - """Match attributes or global python names""" + """Match attributes or global python names. + + .. deprecated:: 8.27 + You can use :meth:`python_matcher` instead.""" if "." in text: try: matches = self.attr_matches(text) diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index 0a524ebe401..937b6e88141 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -14,14 +14,35 @@ - hide frames in tracebacks based on `__tracebackhide__` - allows to skip frames based on `__debuggerskip__` + +Global Configuration +-------------------- + +The IPython debugger will by read the global ``~/.pdbrc`` file. +That is to say you can list all comands supported by ipdb in your `~/.pdbrc` +configuration file, to globally configure pdb. + +Example:: + + # ~/.pdbrc + skip_predicates debuggerskip false + skip_hidden false + context 25 + +Features +-------- + +The IPython debugger can hide and skip frames when printing or moving through +the stack. This can have a performance impact, so can be configures. + The skipping and hiding frames are configurable via the `skip_predicates` command. By default, frames from readonly files will be hidden, frames containing -``__tracebackhide__=True`` will be hidden. +``__tracebackhide__ = True`` will be hidden. -Frames containing ``__debuggerskip__`` will be stepped over, frames who's parent -frames value of ``__debuggerskip__`` is ``True`` will be skipped. +Frames containing ``__debuggerskip__`` will be stepped over, frames whose parent +frames value of ``__debuggerskip__`` is ``True`` will also be skipped. >>> def helpers_helper(): ... pass @@ -1070,7 +1091,9 @@ def do_context(self, context): raise ValueError() self.context = new_context except ValueError: - self.error("The 'context' command requires a positive integer argument.") + self.error( + f"The 'context' command requires a positive integer argument (current value {self.context})." + ) class InterruptiblePdb(Pdb): diff --git a/IPython/core/display.py b/IPython/core/display.py index 20e2e34b8f6..5c4557b150f 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -41,7 +41,11 @@ def __getattr__(name): if name in _deprecated_names: - warn(f"Importing {name} from IPython.core.display is deprecated since IPython 7.14, please import from IPython display", DeprecationWarning, stacklevel=2) + warn( + f"Importing {name} from IPython.core.display is deprecated since IPython 7.14, please import from IPython.display", + DeprecationWarning, + stacklevel=2, + ) return getattr(display_functions, name) if name in globals().keys(): diff --git a/IPython/core/magics/packaging.py b/IPython/core/magics/packaging.py index 093b0a2ec10..09d4117270f 100644 --- a/IPython/core/magics/packaging.py +++ b/IPython/core/magics/packaging.py @@ -9,6 +9,7 @@ #----------------------------------------------------------------------------- import functools +import os import re import shlex import sys @@ -41,6 +42,16 @@ def _get_conda_like_executable(command): executable: string Value should be: conda, mamba or micromamba """ + # Check for a environment variable bound to the base executable, both conda and mamba + # set these when activating an environment. + base_executable = "CONDA_EXE" + if "mamba" in command.lower(): + base_executable = "MAMBA_EXE" + if base_executable in os.environ: + executable = Path(os.environ[base_executable]) + if executable.is_file(): + return str(executable.resolve()) + # Check if there is a conda executable in the same directory as the Python executable. # This is the case within conda's root environment. executable = Path(sys.executable).parent / command @@ -48,10 +59,12 @@ def _get_conda_like_executable(command): return str(executable) # Otherwise, attempt to extract the executable from conda history. - # This applies in any conda environment. + # This applies in any conda environment. Parsing this way is error prone because + # different versions of conda and mamba include differing cmd values such as + # `conda`, `conda-script.py`, or `path/to/conda`, here use the raw command provided. history = Path(sys.prefix, "conda-meta", "history").read_text(encoding="utf-8") match = re.search( - rf"^#\s*cmd:\s*(?P.*{executable})\s[create|install]", + rf"^#\s*cmd:\s*(?P.*{command})\s[create|install]", history, flags=re.MULTILINE, ) diff --git a/IPython/core/magics/pylab.py b/IPython/core/magics/pylab.py index 265f860063a..423498d404c 100644 --- a/IPython/core/magics/pylab.py +++ b/IPython/core/magics/pylab.py @@ -100,7 +100,7 @@ def matplotlib(self, line=''): % _list_matplotlib_backends_and_gui_loops() ) else: - gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui) + gui, backend = self.shell.enable_matplotlib(args.gui) self._show_matplotlib_backend(args.gui, backend) @skip_doctest diff --git a/IPython/core/prefilter.py b/IPython/core/prefilter.py index e611b4b8340..a29df0c27ad 100644 --- a/IPython/core/prefilter.py +++ b/IPython/core/prefilter.py @@ -476,8 +476,8 @@ def check(self, line_info): any python operator, we should simply execute the line (regardless of whether or not there's a possible autocall expansion). This avoids spurious (and very confusing) geattr() accesses.""" - if line_info.the_rest and line_info.the_rest[0] in '!=()<>,+*/%^&|': - return self.prefilter_manager.get_handler_by_name('normal') + if line_info.the_rest and line_info.the_rest[0] in "!=()<>,+*/%^&|": + return self.prefilter_manager.get_handler_by_name("normal") else: return None @@ -512,6 +512,8 @@ def check(self, line_info): callable(oinfo.obj) and (not self.exclude_regexp.match(line_info.the_rest)) and self.function_name_regexp.match(line_info.ifun) + and line_info.raw_the_rest.startswith(" ") + or not line_info.raw_the_rest.strip() ): return self.prefilter_manager.get_handler_by_name("auto") else: diff --git a/IPython/core/profiledir.py b/IPython/core/profiledir.py index 1e33b552fb7..a1b94f2da33 100644 --- a/IPython/core/profiledir.py +++ b/IPython/core/profiledir.py @@ -14,6 +14,8 @@ from ..utils.path import expand_path, ensure_dir_exists from traitlets import Unicode, Bool, observe +from typing import Optional + #----------------------------------------------------------------------------- # Module errors #----------------------------------------------------------------------------- @@ -68,18 +70,31 @@ def _location_changed(self, change): self.pid_dir = os.path.join(new, self.pid_dir_name) self.static_dir = os.path.join(new, self.static_dir_name) self.check_dirs() - - def _mkdir(self, path, mode=None): + + def _mkdir(self, path: str, mode: Optional[int] = None) -> bool: """ensure a directory exists at a given path This is a version of os.mkdir, with the following differences: - - returns True if it created the directory, False otherwise + - returns whether the directory has been created or not. - ignores EEXIST, protecting against race conditions where the dir may have been created in between the check and the creation - sets permissions if requested and the dir already exists + + Parameters + ---------- + path: str + path of the dir to create + mode: int + see `mode` of `os.mkdir` + + Returns + ------- + bool: + returns True if it created the directory, False otherwise """ + if os.path.exists(path): if mode and os.stat(path).st_mode != mode: try: @@ -109,16 +124,20 @@ def check_log_dir(self, change=None): @observe('startup_dir') def check_startup_dir(self, change=None): - self._mkdir(self.startup_dir) - - readme = os.path.join(self.startup_dir, 'README') - src = os.path.join(get_ipython_package_dir(), u'core', u'profile', u'README_STARTUP') - - if not os.path.exists(src): - self.log.warning("Could not copy README_STARTUP to startup dir. Source file %s does not exist.", src) - - if os.path.exists(src) and not os.path.exists(readme): - shutil.copy(src, readme) + if self._mkdir(self.startup_dir): + readme = os.path.join(self.startup_dir, "README") + src = os.path.join( + get_ipython_package_dir(), "core", "profile", "README_STARTUP" + ) + + if os.path.exists(src): + if not os.path.exists(readme): + shutil.copy(src, readme) + else: + self.log.warning( + "Could not copy README_STARTUP to startup dir. Source file %s does not exist.", + src, + ) @observe('security_dir') def check_security_dir(self, change=None): diff --git a/IPython/core/release.py b/IPython/core/release.py index b72524d6ff8..fb5a54da6ab 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,7 +16,7 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 26 +_version_minor = 28 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" diff --git a/IPython/core/splitinput.py b/IPython/core/splitinput.py index 0cd70ec9100..33e462b3b80 100644 --- a/IPython/core/splitinput.py +++ b/IPython/core/splitinput.py @@ -76,7 +76,7 @@ def split_user_input(line, pattern=None): # print('line:<%s>' % line) # dbg # print('pre <%s> ifun <%s> rest <%s>' % (pre,ifun.strip(),the_rest)) # dbg - return pre, esc or '', ifun.strip(), the_rest.lstrip() + return pre, esc or "", ifun.strip(), the_rest class LineInfo(object): @@ -107,11 +107,15 @@ class LineInfo(object): the_rest Everything else on the line. + + raw_the_rest + the_rest without whitespace stripped. """ def __init__(self, line, continue_prompt=False): self.line = line self.continue_prompt = continue_prompt - self.pre, self.esc, self.ifun, self.the_rest = split_user_input(line) + self.pre, self.esc, self.ifun, self.raw_the_rest = split_user_input(line) + self.the_rest = self.raw_the_rest.lstrip() self.pre_char = self.pre.strip() if self.pre_char: @@ -136,3 +140,6 @@ def ofind(self, ip) -> OInfo: def __str__(self): return "LineInfo [%s|%s|%s|%s]" %(self.pre, self.esc, self.ifun, self.the_rest) + + def __repr__(self): + return "<" + str(self) + ">" diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 87c561dc06b..e20d9b88a89 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -462,7 +462,10 @@ def test_all_completions_dups(self): matches = c.all_completions("TestClass.") assert len(matches) > 2, (jedi_status, matches) matches = c.all_completions("TestClass.a") - assert matches == ['TestClass.a', 'TestClass.a1'], jedi_status + if jedi_status: + assert matches == ["TestClass.a", "TestClass.a1"], jedi_status + else: + assert matches == [".a", ".a1"], jedi_status @pytest.mark.xfail( sys.version_info.releaselevel in ("alpha",), @@ -594,7 +597,7 @@ def _(line, cursor_pos, expect, message, completion): ip.Completer.use_jedi = True with provisionalcompleter(): completions = ip.Completer.completions(line, cursor_pos) - self.assertIn(completion, completions) + self.assertIn(completion, list(completions)) with provisionalcompleter(): _( @@ -622,7 +625,7 @@ def _(line, cursor_pos, expect, message, completion): _( "assert str.star", 14, - "str.startswith", + ".startswith", "Should have completed on `assert str.star`: %s", Completion(11, 14, "startswith"), ) @@ -633,6 +636,13 @@ def _(line, cursor_pos, expect, message, completion): "Should have completed on `d['a b'].str`: %s", Completion(9, 12, "strip"), ) + _( + "a.app", + 4, + ".append", + "Should have completed on `a.app`: %s", + Completion(2, 4, "append"), + ) def test_omit__names(self): # also happens to test IPCompleter as a configurable @@ -647,8 +657,8 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip.") - self.assertIn("ip.__str__", matches) - self.assertIn("ip._hidden_attr", matches) + self.assertIn(".__str__", matches) + self.assertIn("._hidden_attr", matches) # c.use_jedi = True # completions = set(c.completions('ip.', 3)) @@ -661,7 +671,7 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip.") - self.assertNotIn("ip.__str__", matches) + self.assertNotIn(".__str__", matches) # self.assertIn('ip._hidden_attr', matches) # c.use_jedi = True @@ -675,8 +685,8 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip.") - self.assertNotIn("ip.__str__", matches) - self.assertNotIn("ip._hidden_attr", matches) + self.assertNotIn(".__str__", matches) + self.assertNotIn("._hidden_attr", matches) # c.use_jedi = True # completions = set(c.completions('ip.', 3)) @@ -686,7 +696,7 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip._x.") - self.assertIn("ip._x.keys", matches) + self.assertIn(".keys", matches) # c.use_jedi = True # completions = set(c.completions('ip._x.', 6)) @@ -697,7 +707,7 @@ def test_omit__names(self): def test_limit_to__all__False_ok(self): """ - Limit to all is deprecated, once we remove it this test can go away. + Limit to all is deprecated, once we remove it this test can go away. """ ip = get_ipython() c = ip.Completer @@ -708,7 +718,7 @@ def test_limit_to__all__False_ok(self): cfg.IPCompleter.limit_to__all__ = False c.update_config(cfg) s, matches = c.complete("d.") - self.assertIn("d.x", matches) + self.assertIn(".x", matches) def test_get__all__entries_ok(self): class A: diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index b22be9231dc..6113ff920c4 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -59,40 +59,43 @@ def test_ipdb_magics(): First, set up some test functions and classes which we can inspect. - >>> class ExampleClass(object): - ... """Docstring for ExampleClass.""" - ... def __init__(self): - ... """Docstring for ExampleClass.__init__""" - ... pass - ... def __str__(self): - ... return "ExampleClass()" - - >>> def example_function(x, y, z="hello"): - ... """Docstring for example_function.""" - ... pass + In [1]: class ExampleClass(object): + ...: """Docstring for ExampleClass.""" + ...: def __init__(self): + ...: """Docstring for ExampleClass.__init__""" + ...: pass + ...: def __str__(self): + ...: return "ExampleClass()" - >>> old_trace = sys.gettrace() + In [2]: def example_function(x, y, z="hello"): + ...: """Docstring for example_function.""" + ...: pass - Create a function which triggers ipdb. + In [3]: old_trace = sys.gettrace() - >>> def trigger_ipdb(): - ... a = ExampleClass() - ... debugger.Pdb().set_trace() + Create a function which triggers ipdb. - >>> with PdbTestInput([ - ... 'pdef example_function', - ... 'pdoc ExampleClass', - ... 'up', - ... 'down', - ... 'list', - ... 'pinfo a', - ... 'll', - ... 'continue', - ... ]): - ... trigger_ipdb() - --Return-- - None - > (3)trigger_ipdb() + In [4]: def trigger_ipdb(): + ...: a = ExampleClass() + ...: debugger.Pdb().set_trace() + + Run ipdb with faked input & check output. Because of a difference between + Python 3.13 & older versions, the first bit of the output is inconsistent. + We need to use ... to accommodate that, so the examples have to use IPython + prompts so that ... is distinct from the Python PS2 prompt. + + In [5]: with PdbTestInput([ + ...: 'pdef example_function', + ...: 'pdoc ExampleClass', + ...: 'up', + ...: 'down', + ...: 'list', + ...: 'pinfo a', + ...: 'll', + ...: 'continue', + ...: ]): + ...: trigger_ipdb() + ...> (3)trigger_ipdb() 1 def trigger_ipdb(): 2 a = ExampleClass() ----> 3 debugger.Pdb().set_trace() @@ -112,8 +115,7 @@ def test_ipdb_magics(): 10 ]): ---> 11 trigger_ipdb() - ipdb> down - None + ipdb> down... > (3)trigger_ipdb() 1 def trigger_ipdb(): 2 a = ExampleClass() @@ -136,10 +138,10 @@ def test_ipdb_magics(): ----> 3 debugger.Pdb().set_trace() ipdb> continue - - Restore previous trace function, e.g. for coverage.py - - >>> sys.settrace(old_trace) + + Restore previous trace function, e.g. for coverage.py + + In [6]: sys.settrace(old_trace) ''' def test_ipdb_magics2(): @@ -495,15 +497,26 @@ def test_decorator_skip_with_breakpoint(): child.expect_exact(line) child.sendline("") - # as the filename does not exists, we'll rely on the filename prompt - child.expect_exact("47 bar(3, 4)") - - for input_, expected in [ - (f"b {name}.py:3", ""), - ("step", "1---> 3 pass # should not stop here except"), - ("step", "---> 38 @pdb_skipped_decorator"), - ("continue", ""), - ]: + # From 3.13, set_trace()/breakpoint() stop on the line where they're + # called, instead of the next line. + if sys.version_info >= (3, 13): + child.expect_exact("--> 46 ipdb.set_trace()") + extra_step = [("step", "--> 47 bar(3, 4)")] + else: + child.expect_exact("--> 47 bar(3, 4)") + extra_step = [] + + for input_, expected in ( + [ + (f"b {name}.py:3", ""), + ] + + extra_step + + [ + ("step", "1---> 3 pass # should not stop here except"), + ("step", "---> 38 @pdb_skipped_decorator"), + ("continue", ""), + ] + ): child.expect("ipdb>") child.sendline(input_) child.expect_exact(input_) diff --git a/IPython/core/tests/test_handlers.py b/IPython/core/tests/test_handlers.py index 604dadee1ab..905d9abe07b 100644 --- a/IPython/core/tests/test_handlers.py +++ b/IPython/core/tests/test_handlers.py @@ -7,17 +7,13 @@ # our own packages from IPython.core import autocall from IPython.testing import tools as tt +import pytest +from collections.abc import Callable #----------------------------------------------------------------------------- # Globals #----------------------------------------------------------------------------- -# Get the public instance of IPython - -failures = [] -num_tests = 0 - -#----------------------------------------------------------------------------- # Test functions #----------------------------------------------------------------------------- @@ -31,67 +27,49 @@ def __call__(self): return "called" -def run(tests): - """Loop through a list of (pre, post) inputs, where pre is the string - handed to ipython, and post is how that string looks after it's been - transformed (i.e. ipython's notion of _i)""" - tt.check_pairs(ip.prefilter_manager.prefilter_lines, tests) - +@pytest.mark.parametrize( + "autocall, input, output", + [ + # For many of the below, we're also checking that leading whitespace + # turns off the esc char, which it should unless there is a continuation + # line. + ("1", '"no change"', '"no change"'), # normal + ("1", "lsmagic", "get_ipython().run_line_magic('lsmagic', '')"), # magic + # Only explicit escapes or instances of IPyAutocallable should get + # expanded + ("0", 'len "abc"', 'len "abc"'), + ("0", "autocallable", "autocallable()"), + # Don't add extra brackets (gh-1117) + ("0", "autocallable()", "autocallable()"), + ("1", 'len "abc"', 'len("abc")'), + ("1", 'len "abc";', 'len("abc");'), # ; is special -- moves out of parens + # Autocall is turned off if first arg is [] and the object + # is both callable and indexable. Like so: + ("1", "len [1,2]", "len([1,2])"), # len doesn't support __getitem__... + ("1", "call_idx [1]", "call_idx [1]"), # call_idx *does*.. + ("1", "call_idx 1", "call_idx(1)"), + ("1", "len", "len"), # only at 2 does it auto-call on single args + ("2", 'len "abc"', 'len("abc")'), + ("2", 'len "abc";', 'len("abc");'), + ("2", "len [1,2]", "len([1,2])"), + ("2", "call_idx [1]", "call_idx [1]"), + ("2", "call_idx 1", "call_idx(1)"), + # T his is what's different: + ("2", "len", "len()"), # only at 2 does it auto-call on single args + ("0", "Callable[[int], None]", "Callable[[int], None]"), + ("1", "Callable[[int], None]", "Callable[[int], None]"), + ("1", "Callable[[int], None]", "Callable[[int], None]"), + ], +) +def test_handlers_I(autocall, input, output): + autocallable = Autocallable() + ip.user_ns["autocallable"] = autocallable -def test_handlers(): call_idx = CallableIndexable() - ip.user_ns['call_idx'] = call_idx + ip.user_ns["call_idx"] = call_idx - # For many of the below, we're also checking that leading whitespace - # turns off the esc char, which it should unless there is a continuation - # line. - run( - [('"no change"', '"no change"'), # normal - (u"lsmagic", "get_ipython().run_line_magic('lsmagic', '')"), # magic - #("a = b # PYTHON-MODE", '_i'), # emacs -- avoids _in cache - ]) + ip.user_ns["Callable"] = Callable - # Objects which are instances of IPyAutocall are *always* autocalled - autocallable = Autocallable() - ip.user_ns['autocallable'] = autocallable - - # auto - ip.run_line_magic("autocall", "0") - # Only explicit escapes or instances of IPyAutocallable should get - # expanded - run( - [ - ('len "abc"', 'len "abc"'), - ("autocallable", "autocallable()"), - # Don't add extra brackets (gh-1117) - ("autocallable()", "autocallable()"), - ] - ) + ip.run_line_magic("autocall", autocall) + assert ip.prefilter_manager.prefilter_lines(input) == output ip.run_line_magic("autocall", "1") - run( - [ - ('len "abc"', 'len("abc")'), - ('len "abc";', 'len("abc");'), # ; is special -- moves out of parens - # Autocall is turned off if first arg is [] and the object - # is both callable and indexable. Like so: - ("len [1,2]", "len([1,2])"), # len doesn't support __getitem__... - ("call_idx [1]", "call_idx [1]"), # call_idx *does*.. - ("call_idx 1", "call_idx(1)"), - ("len", "len"), # only at 2 does it auto-call on single args - ] - ) - ip.run_line_magic("autocall", "2") - run( - [ - ('len "abc"', 'len("abc")'), - ('len "abc";', 'len("abc");'), - ("len [1,2]", "len([1,2])"), - ("call_idx [1]", "call_idx [1]"), - ("call_idx 1", "call_idx(1)"), - # This is what's different: - ("len", "len()"), # only at 2 does it auto-call on single args - ] - ) - ip.run_line_magic("autocall", "1") - - assert failures == [] diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index 3decb8be04b..ac7c3656099 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -516,23 +516,23 @@ def prop(self, v): ip.run_line_magic("pinfo", "b.prop") captured = capsys.readouterr() - assert "Docstring: cdoc for prop" in captured.out + assert re.search(r"Docstring:\s+cdoc for prop", captured.out) ip.run_line_magic("pinfo", "b.non_exist") captured = capsys.readouterr() - assert "Docstring: cdoc for non_exist" in captured.out + assert re.search(r"Docstring:\s+cdoc for non_exist", captured.out) ip.run_cell("b.prop?") captured = capsys.readouterr() - assert "Docstring: cdoc for prop" in captured.out + assert re.search(r"Docstring:\s+cdoc for prop", captured.out) ip.run_cell("b.non_exist?") captured = capsys.readouterr() - assert "Docstring: cdoc for non_exist" in captured.out + assert re.search(r"Docstring:\s+cdoc for non_exist", captured.out) ip.run_cell("b.undefined?") captured = capsys.readouterr() - assert "Docstring: " in captured.out + assert re.search(r"Type:\s+NoneType", captured.out) def test_pinfo_magic(): diff --git a/IPython/core/tests/test_prefilter.py b/IPython/core/tests/test_prefilter.py index 91c3c868821..999cd43e6e8 100644 --- a/IPython/core/tests/test_prefilter.py +++ b/IPython/core/tests/test_prefilter.py @@ -48,7 +48,7 @@ def dummy_magic(line): pass def test_autocall_binops(): """See https://github.com/ipython/ipython/issues/81""" - ip.magic('autocall 2') + ip.run_line_magic("autocall", "2") f = lambda x: x ip.user_ns['f'] = f try: @@ -71,8 +71,8 @@ def test_autocall_binops(): finally: pm.unregister_checker(ac) finally: - ip.magic('autocall 0') - del ip.user_ns['f'] + ip.run_line_magic("autocall", "0") + del ip.user_ns["f"] def test_issue_114(): @@ -105,23 +105,35 @@ def __call__(self, x): return x # Create a callable broken object - ip.user_ns['x'] = X() - ip.magic('autocall 2') + ip.user_ns["x"] = X() + ip.run_line_magic("autocall", "2") try: # Even if x throws an attribute error when looking at its rewrite # attribute, we should not crash. So the test here is simply making # the prefilter call and not having an exception. ip.prefilter('x 1') finally: - del ip.user_ns['x'] - ip.magic('autocall 0') + del ip.user_ns["x"] + ip.run_line_magic("autocall", "0") + + +def test_autocall_type_ann(): + ip.run_cell("import collections.abc") + ip.run_line_magic("autocall", "1") + try: + assert ( + ip.prefilter("collections.abc.Callable[[int], None]") + == "collections.abc.Callable[[int], None]" + ) + finally: + ip.run_line_magic("autocall", "0") def test_autocall_should_support_unicode(): - ip.magic('autocall 2') - ip.user_ns['π'] = lambda x: x + ip.run_line_magic("autocall", "2") + ip.user_ns["π"] = lambda x: x try: assert ip.prefilter("π 3") == "π(3)" finally: - ip.magic('autocall 0') - del ip.user_ns['π'] + ip.run_line_magic("autocall", "0") + del ip.user_ns["π"] diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py index 6bddb348077..31d3dbe21f2 100644 --- a/IPython/core/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -258,6 +258,29 @@ def test_qt_gtk(self): assert gui == "qt" assert s.pylab_gui_select == "qt" + @dec.skipif(not pt._matplotlib_manages_backends()) + def test_backend_module_name_case_sensitive(self): + # Matplotlib backend names are case insensitive unless explicitly specified using + # "module://some_module.some_name" syntax which are case sensitive for mpl >= 3.9.1 + all_lowercase = "module://matplotlib_inline.backend_inline" + some_uppercase = "module://matplotlib_inline.Backend_inline" + mpl3_9_1 = matplotlib.__version_info__ >= (3, 9, 1) + + s = self.Shell() + s.enable_matplotlib(all_lowercase) + if mpl3_9_1: + with pytest.raises(RuntimeError): + s.enable_matplotlib(some_uppercase) + else: + s.enable_matplotlib(some_uppercase) + + s.run_line_magic("matplotlib", all_lowercase) + if mpl3_9_1: + with pytest.raises(RuntimeError): + s.run_line_magic("matplotlib", some_uppercase) + else: + s.run_line_magic("matplotlib", some_uppercase) + def test_no_gui_backends(): for k in ['agg', 'svg', 'pdf', 'ps']: diff --git a/IPython/core/tests/test_splitinput.py b/IPython/core/tests/test_splitinput.py index 1462e7fa033..f5fc53fafe3 100644 --- a/IPython/core/tests/test_splitinput.py +++ b/IPython/core/tests/test_splitinput.py @@ -1,7 +1,8 @@ # coding: utf-8 from IPython.core.splitinput import split_user_input, LineInfo -from IPython.testing import tools as tt + +import pytest tests = [ ("x=1", ("", "", "x", "=1")), @@ -19,18 +20,19 @@ (";ls", ("", ";", "ls", "")), (" ;ls", (" ", ";", "ls", "")), ("f.g(x)", ("", "", "f.g", "(x)")), - ("f.g (x)", ("", "", "f.g", "(x)")), + ("f.g (x)", ("", "", "f.g", " (x)")), ("?%hist1", ("", "?", "%hist1", "")), ("?%%hist2", ("", "?", "%%hist2", "")), ("??%hist3", ("", "??", "%hist3", "")), ("??%%hist4", ("", "??", "%%hist4", "")), ("?x*", ("", "?", "x*", "")), + ("Pérez Fernando", ("", "", "Pérez", " Fernando")), ] -tests.append(("Pérez Fernando", ("", "", "Pérez", "Fernando"))) -def test_split_user_input(): - return tt.check_pairs(split_user_input, tests) +@pytest.mark.parametrize("input, output", tests) +def test_split_user_input(input, output): + assert split_user_input(input) == output def test_LineInfo(): diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index e167d99506a..8ed73873aa1 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -298,6 +298,13 @@ class Python3ChainedExceptionsTest(unittest.TestCase): raise ValueError("Yikes") from None """ + SYS_EXIT_WITH_CONTEXT_CODE = """ +try: + 1/0 +except Exception as e: + raise SystemExit(1) + """ + def test_direct_cause_error(self): with tt.AssertPrints(["KeyError", "NameError", "direct cause"]): ip.run_cell(self.DIRECT_CAUSE_ERROR_CODE) @@ -306,6 +313,11 @@ def test_exception_during_handling_error(self): with tt.AssertPrints(["KeyError", "NameError", "During handling"]): ip.run_cell(self.EXCEPTION_DURING_HANDLING_CODE) + def test_sysexit_while_handling_error(self): + with tt.AssertPrints(["SystemExit", "to see the full traceback"]): + with tt.AssertNotPrints(["another exception"], suppress=False): + ip.run_cell(self.SYS_EXIT_WITH_CONTEXT_CODE) + def test_suppress_exception_chaining(self): with tt.AssertNotPrints("ZeroDivisionError"), \ tt.AssertPrints("ValueError", suppress=False): diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index cc139b1e257..a8a38f165b7 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -552,28 +552,31 @@ def structured_traceback( lines = ''.join(self._format_exception_only(etype, evalue)) out_list.append(lines) - exception = self.get_parts_of_chained_exception(evalue) + # Find chained exceptions if we have a traceback (not for exception-only mode) + if etb is not None: + exception = self.get_parts_of_chained_exception(evalue) - if exception and (id(exception[1]) not in chained_exc_ids): - chained_exception_message = ( - self.prepare_chained_exception_message(evalue.__cause__)[0] - if evalue is not None - else "" - ) - etype, evalue, etb = exception - # Trace exception to avoid infinite 'cause' loop - chained_exc_ids.add(id(exception[1])) - chained_exceptions_tb_offset = 0 - out_list = ( - self.structured_traceback( - etype, - evalue, - (etb, chained_exc_ids), # type: ignore - chained_exceptions_tb_offset, - context, + if exception and (id(exception[1]) not in chained_exc_ids): + chained_exception_message = ( + self.prepare_chained_exception_message(evalue.__cause__)[0] + if evalue is not None + else "" + ) + etype, evalue, etb = exception + # Trace exception to avoid infinite 'cause' loop + chained_exc_ids.add(id(exception[1])) + chained_exceptions_tb_offset = 0 + out_list = ( + self.structured_traceback( + etype, + evalue, + (etb, chained_exc_ids), # type: ignore + chained_exceptions_tb_offset, + context, + ) + + chained_exception_message + + out_list ) - + chained_exception_message - + out_list) return out_list @@ -827,8 +830,8 @@ class VerboseTB(TBTools): traceback, to be used with alternate interpreters (because their own code would appear in the traceback).""" - _tb_highlight = "bg:ansiyellow" - _tb_highlight_style = "default" + tb_highlight = "bg:ansiyellow" + tb_highlight_style = "default" def __init__( self, @@ -1130,8 +1133,8 @@ def get_records( after = context // 2 before = context - after if self.has_colors: - style = get_style_by_name(self._tb_highlight_style) - style = stack_data.style_with_executing_node(style, self._tb_highlight) + style = get_style_by_name(self.tb_highlight_style) + style = stack_data.style_with_executing_node(style, self.tb_highlight) formatter = Terminal256Formatter(style=style) else: formatter = None diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index 1486cf9d773..6058ee5a9a8 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -302,13 +302,25 @@ def import_pyside6(): ImportErrors raised within this function are non-recoverable """ + + def get_attrs(module): + return { + name: getattr(module, name) + for name in dir(module) + if not name.startswith("_") + } + from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport # Join QtGui and QtWidgets for Qt4 compatibility. QtGuiCompat = types.ModuleType("QtGuiCompat") QtGuiCompat.__dict__.update(QtGui.__dict__) - QtGuiCompat.__dict__.update(QtWidgets.__dict__) - QtGuiCompat.__dict__.update(QtPrintSupport.__dict__) + if QtCore.__version_info__ < (6, 7): + QtGuiCompat.__dict__.update(QtWidgets.__dict__) + QtGuiCompat.__dict__.update(QtPrintSupport.__dict__) + else: + QtGuiCompat.__dict__.update(get_attrs(QtWidgets)) + QtGuiCompat.__dict__.update(get_attrs(QtPrintSupport)) return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6 diff --git a/IPython/utils/terminal.py b/IPython/utils/terminal.py index b09cfe0d22d..10d73fce586 100644 --- a/IPython/utils/terminal.py +++ b/IPython/utils/terminal.py @@ -80,7 +80,13 @@ def _set_term_title_xterm(title): def _restore_term_title_xterm(): # Make sure the restore has at least one accompanying set. global _xterm_term_title_saved - assert _xterm_term_title_saved + if not _xterm_term_title_saved: + warnings.warn( + "Expecting xterm_term_title_saved to be True, but is not; will not restore terminal title.", + stacklevel=1, + ) + return + sys.stdout.write('\033[23;0t') _xterm_term_title_saved = False diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index 5ea27e08693..92794b690de 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -75,20 +75,29 @@ def teardown_module(): shutil.rmtree(TMP_TEST_DIR) -def setup_environment(): - """Setup testenvironment for some functions that are tested - in this module. In particular this functions stores attributes - and other things that we need to stub in some test functions. - This needs to be done on a function level and not module level because - each testfunction needs a pristine environment. - """ +# Build decorator that uses the setup_environment/setup_environment +@pytest.fixture +def environment(): global oldstuff, platformstuff - oldstuff = (env.copy(), os.name, sys.platform, path.get_home_dir, IPython.__file__, os.getcwd()) + oldstuff = ( + env.copy(), + os.name, + sys.platform, + path.get_home_dir, + IPython.__file__, + os.getcwd(), + ) -def teardown_environment(): - """Restore things that were remembered by the setup_environment function - """ - (oldenv, os.name, sys.platform, path.get_home_dir, IPython.__file__, old_wd) = oldstuff + yield + + ( + oldenv, + os.name, + sys.platform, + path.get_home_dir, + IPython.__file__, + old_wd, + ) = oldstuff os.chdir(old_wd) reload(path) @@ -96,16 +105,7 @@ def teardown_environment(): if key not in oldenv: del env[key] env.update(oldenv) - if hasattr(sys, 'frozen'): - del sys.frozen - - -# Build decorator that uses the setup_environment/setup_environment -@pytest.fixture -def environment(): - setup_environment() - yield - teardown_environment() + assert not hasattr(sys, "frozen") with_environment = pytest.mark.usefixtures("environment") @@ -113,11 +113,11 @@ def environment(): @skip_if_not_win32 @with_environment -def test_get_home_dir_1(): +def test_get_home_dir_1(monkeypatch): """Testcase for py2exe logic, un-compressed lib """ unfrozen = path.get_home_dir() - sys.frozen = True + monkeypatch.setattr(sys, "frozen", True, raising=False) #fake filename for IPython.__init__ IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py")) @@ -128,13 +128,15 @@ def test_get_home_dir_1(): @skip_if_not_win32 @with_environment -def test_get_home_dir_2(): +def test_get_home_dir_2(monkeypatch): """Testcase for py2exe logic, compressed lib """ unfrozen = path.get_home_dir() - sys.frozen = True - #fake filename for IPython.__init__ - IPython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py")).lower() + monkeypatch.setattr(sys, "frozen", True, raising=False) + # fake filename for IPython.__init__ + IPython.__file__ = abspath( + join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py") + ).lower() home_dir = path.get_home_dir(True) assert home_dir == unfrozen @@ -160,22 +162,22 @@ def test_get_home_dir_4(): @skip_win32 @with_environment -def test_get_home_dir_5(): +def test_get_home_dir_5(monkeypatch): """raise HomeDirError if $HOME is specified, but not a writable dir""" env['HOME'] = abspath(HOME_TEST_DIR+'garbage') # set os.name = posix, to prevent My Documents fallback on Windows - os.name = 'posix' + monkeypatch.setattr(os, "name", "posix") pytest.raises(path.HomeDirError, path.get_home_dir, True) # Should we stub wreg fully so we can run the test on all platforms? @skip_if_not_win32 @with_environment -def test_get_home_dir_8(): +def test_get_home_dir_8(monkeypatch): """Using registry hack for 'My Documents', os=='nt' HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing. """ - os.name = 'nt' + monkeypatch.setattr(os, "name", "nt") # Remove from stub environment all keys that may be set for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']: env.pop(key, None) @@ -194,13 +196,12 @@ def __exit__(*args, **kwargs): assert home_dir == abspath(HOME_TEST_DIR) @with_environment -def test_get_xdg_dir_0(): +def test_get_xdg_dir_0(monkeypatch): """test_get_xdg_dir_0, check xdg_dir""" - reload(path) - path._writable_dir = lambda path: True - path.get_home_dir = lambda : 'somewhere' - os.name = "posix" - sys.platform = "linux2" + monkeypatch.setattr(path, "_writable_dir", lambda path: True) + monkeypatch.setattr(path, "get_home_dir", lambda: "somewhere") + monkeypatch.setattr(os, "name", "posix") + monkeypatch.setattr(sys, "platform", "linux2") env.pop('IPYTHON_DIR', None) env.pop('IPYTHONDIR', None) env.pop('XDG_CONFIG_HOME', None) @@ -209,44 +210,41 @@ def test_get_xdg_dir_0(): @with_environment -def test_get_xdg_dir_1(): +def test_get_xdg_dir_1(monkeypatch): """test_get_xdg_dir_1, check nonexistent xdg_dir""" - reload(path) - path.get_home_dir = lambda : HOME_TEST_DIR - os.name = "posix" - sys.platform = "linux2" - env.pop('IPYTHON_DIR', None) - env.pop('IPYTHONDIR', None) - env.pop('XDG_CONFIG_HOME', None) + monkeypatch.setattr(path, "get_home_dir", lambda: HOME_TEST_DIR) + monkeypatch.setattr(os, "name", "posix") + monkeypatch.setattr(sys, "platform", "linux2") + env.pop("IPYTHON_DIR", None) + env.pop("IPYTHONDIR", None) + env.pop("XDG_CONFIG_HOME", None) assert path.get_xdg_dir() is None @with_environment -def test_get_xdg_dir_2(): +def test_get_xdg_dir_2(monkeypatch): """test_get_xdg_dir_2, check xdg_dir default to ~/.config""" - reload(path) - path.get_home_dir = lambda : HOME_TEST_DIR - os.name = "posix" - sys.platform = "linux2" - env.pop('IPYTHON_DIR', None) - env.pop('IPYTHONDIR', None) - env.pop('XDG_CONFIG_HOME', None) - cfgdir=os.path.join(path.get_home_dir(), '.config') + monkeypatch.setattr(path, "get_home_dir", lambda: HOME_TEST_DIR) + monkeypatch.setattr(os, "name", "posix") + monkeypatch.setattr(sys, "platform", "linux2") + env.pop("IPYTHON_DIR", None) + env.pop("IPYTHONDIR", None) + env.pop("XDG_CONFIG_HOME", None) + cfgdir = os.path.join(path.get_home_dir(), ".config") if not os.path.exists(cfgdir): os.makedirs(cfgdir) assert path.get_xdg_dir() == cfgdir @with_environment -def test_get_xdg_dir_3(): +def test_get_xdg_dir_3(monkeypatch): """test_get_xdg_dir_3, check xdg_dir not used on non-posix systems""" - reload(path) - path.get_home_dir = lambda : HOME_TEST_DIR - os.name = "nt" - sys.platform = "win32" - env.pop('IPYTHON_DIR', None) - env.pop('IPYTHONDIR', None) - env.pop('XDG_CONFIG_HOME', None) - cfgdir=os.path.join(path.get_home_dir(), '.config') + monkeypatch.setattr(path, "get_home_dir", lambda: HOME_TEST_DIR) + monkeypatch.setattr(os, "name", "nt") + monkeypatch.setattr(sys, "platform", "win32") + env.pop("IPYTHON_DIR", None) + env.pop("IPYTHONDIR", None) + env.pop("XDG_CONFIG_HOME", None) + cfgdir = os.path.join(path.get_home_dir(), ".config") os.makedirs(cfgdir, exist_ok=True) assert path.get_xdg_dir() is None @@ -281,31 +279,30 @@ def test_get_long_path_name(): assert p == "/usr/local" -class TestRaiseDeprecation(unittest.TestCase): +@dec.skip_win32 # can't create not-user-writable dir on win +@with_environment +def test_not_writable_ipdir(): + tmpdir = tempfile.mkdtemp() + os.name = "posix" + env.pop("IPYTHON_DIR", None) + env.pop("IPYTHONDIR", None) + env.pop("XDG_CONFIG_HOME", None) + env["HOME"] = tmpdir + ipdir = os.path.join(tmpdir, ".ipython") + os.mkdir(ipdir, 0o555) + try: + open(os.path.join(ipdir, "_foo_"), "w", encoding="utf-8").close() + except IOError: + pass + else: + # I can still write to an unwritable dir, + # assume I'm root and skip the test + pytest.skip("I can't create directories that I can't write to") + + with pytest.warns(UserWarning, match="is not a writable location"): + ipdir = paths.get_ipython_dir() + env.pop("IPYTHON_DIR", None) - @dec.skip_win32 # can't create not-user-writable dir on win - @with_environment - def test_not_writable_ipdir(self): - tmpdir = tempfile.mkdtemp() - os.name = "posix" - env.pop('IPYTHON_DIR', None) - env.pop('IPYTHONDIR', None) - env.pop('XDG_CONFIG_HOME', None) - env['HOME'] = tmpdir - ipdir = os.path.join(tmpdir, '.ipython') - os.mkdir(ipdir, 0o555) - try: - open(os.path.join(ipdir, "_foo_"), "w", encoding="utf-8").close() - except IOError: - pass - else: - # I can still write to an unwritable dir, - # assume I'm root and skip the test - pytest.skip("I can't create directories that I can't write to") - - with self.assertWarnsRegex(UserWarning, 'is not a writable location'): - ipdir = paths.get_ipython_dir() - env.pop('IPYTHON_DIR', None) @with_environment def test_get_py_filename(): diff --git a/docs/source/config/custommagics.rst b/docs/source/config/custommagics.rst index 0a37b858a4c..4854970ef31 100644 --- a/docs/source/config/custommagics.rst +++ b/docs/source/config/custommagics.rst @@ -141,7 +141,7 @@ Accessing user namespace and local scope When creating line magics, you may need to access surrounding scope to get user variables (e.g when called inside functions). IPython provides the ``@needs_local_scope`` decorator that can be imported from -``IPython.core.magics``. When decorated with ``@needs_local_scope`` a magic will +``IPython.core.magic``. When decorated with ``@needs_local_scope`` a magic will be passed ``local_ns`` as an argument. As a convenience ``@needs_local_scope`` can also be applied to cell magics even if cell magics cannot appear at local scope context. @@ -153,7 +153,7 @@ Sometimes it may be useful to define a magic that can be silenced the same way that non-magic expressions can, i.e., by appending a semicolon at the end of the Python code to be executed. That can be achieved by decorating the magic function with the decorator ``@output_can_be_silenced`` that can be imported from -``IPython.core.magics``. When this decorator is used, IPython will parse the Python +``IPython.core.magic``. When this decorator is used, IPython will parse the Python code used by the magic and, if the last token is a ``;``, the output created by the magic will not show up on the screen. If you want to see an example of this decorator in action, take a look on the ``time`` magic defined in diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 490bec850ea..c231777a26a 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -1,6 +1,74 @@ ============ 8.x Series ============ +.. _version 8.28: + +IPython 8.28 +============ + +Slight delay of this September release as I was busy at Pydata Paris last week. +Not many user visible changes for this release, a couple of bug fixes and +workaround: + + - :ghpull:`14480` AssertionError: assert _xterm_term_title_saved in WSL – It is + unclear why the terminal title is not saved in WSL, if you've WSL experience + we'd love your feedback and help to not just ignore an error + - :ghpull:`14510` Fix use of pyside6 >= 6.7.0 + - :ghpull:`14518` Make values public (_tb_highlight & _tb_highlight_style) + - :ghpull:`14515` Use environment variable to identify conda / mamba + + +As usual you can find the full list of PRs on GitHub under `the 8.28 +`__ milestone. + +For something completely different +---------------------------------- + +One of the first works of Science Fiction (`Frankenstein +`__), was written by `Mary Shelley +`__ when she was 18, before being +published in London on 1 January 1818 when she was 20. This is often overlooked, +and the role of founders of science fiction attribute to Edgar Allan Poe and +Jules Verne despite being published later. + +Thanks +------ + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + + +.. _version 8.27: + +IPython 8.27 +============ + +New release of IPython after a month off (not enough changes). We can see a few +important changes for this release. + + - autocall was beeing call getitem, :ghpull:`14486` + - Only copy files in startup dir if we just created it. :ghpull:`14497` + - Fix some tests on Python 3.13 RC1 :ghpull:`14504`; this one I guess make this + the first IPython release officially compatible with Python 3.13; you will + need the most recent ``executing`` and ``stack_data``, we won't pin to avoid + forcing user of older Python version to upgrade. + + +As usual you can find the full list of PRs on GitHub under `the 8.27 +`__ milestone. + +Thanks +------ + +Many thanks to `@Kleirre `__ our June intern for +doing her first contribution to open source, doing the releases notes and +release. I guess you didn't even notice it was not me who released :-). I wish +her all the best in her future endeavor and look forward for her work in +astrophysics. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + .. _version 8.26: IPython 8.26 @@ -361,16 +429,27 @@ Reverted in 8.17.1: - :ghpull:`14190` remove support for python 2 in lexers (reverted in 8.17.1 as it is imported by qtconsole/spyder) -Mamba and Micromamba magic -~~~~~~~~~~~~~~~~~~~~~~~~~~ -In addition to the conda command to manage conda environment, mamba and -micromamba can now be used using the corresponding magic in IPython. -Since these commands are compatible with conda, they are following the -same logic. +Mamba and Micromamba magic commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to the ``%conda`` magic command for calling ``conda`` in IPython, +the ``%mamba`` and ``%micromamba`` magic commands now +call ``mamba`` and ``micromamba`` if they are on ``sys.path``. + +.. code:: + + %mamba install pkgname + %micromamba install pkgname + %conda install pkgname + %pip install pkgname + + %mamba --help + %micromamba --help + %conda --help + %pip --help # works w/ JupyterLite + !pip --help -These two magic require to have the corresponding commands available -either in the conda environment or system wide. :ghpull:`14191` diff --git a/tools/release b/tools/release index ffc7e63cb65..5164a2bb699 100755 --- a/tools/release +++ b/tools/release @@ -1,86 +1,10 @@ #!/usr/bin/env python3 """IPython release script. -This should ONLY be run at real release time. -""" -from __future__ import print_function - -import os -from glob import glob -from pathlib import Path -from subprocess import call -import sys - -from toollib import (get_ipdir, cd, execfile, sh, archive, - archive_user, archive_dir) - -# Get main ipython dir, this will raise if it doesn't pass some checks -ipdir = get_ipdir() -tooldir = ipdir / 'tools' -distdir = ipdir / 'dist' - -# Where I keep static backups of each release -ipbackupdir = Path('~/ipython/backup').expanduser() -if not ipbackupdir.exists(): - ipbackupdir.mkdir(parents=True, exist_ok=True) - -# Start in main IPython dir -cd(ipdir) - -# Load release info -version = None -execfile(Path('IPython','core','release.py'), globals()) - -# Build site addresses for file uploads -release_site = '%s/release/%s' % (archive, version) -backup_site = '%s/backup/' % archive - -# Start actual release process -print() -print('Releasing IPython') -print('=================') -print() -print('Version:', version) -print() -print('Source IPython directory:', ipdir) -print() - -# Perform local backup, go to tools dir to run it. -cd(tooldir) +Deprecated -if 'upload' in sys.argv: - cd(distdir) - - # do not upload OS specific files like .DS_Store - to_upload = glob('*.whl')+glob('*.tar.gz') - - # Make target dir if it doesn't exist - print('1. Uploading IPython to archive.ipython.org') - sh('ssh %s "mkdir -p %s/release/%s" ' % (archive_user, archive_dir, version)) - sh('scp *.tar.gz *.whl %s' % release_site) - - print('2. Uploading backup files...') - cd(ipbackupdir) - sh('scp `ls -1tr *tgz | tail -1` %s' % backup_site) - - print('3. Uploading to PyPI using twine') - cd(distdir) - call(['twine', 'upload', '--verbose'] + to_upload) - -else: - # Build, but don't upload - - # Make backup tarball - sh('python make_tarball.py') - sh('mv ipython-*.tgz %s' % ipbackupdir) - - # Build release files - sh('./build_release') - - cd(ipdir) - - print("`./release upload` to upload source distribution on PyPI and ipython archive") - sys.exit(0) +""" +sys.exit("deprecated") diff --git a/tools/release_helper.sh b/tools/release_helper.sh index df1ca0d48d2..973baacf14f 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -30,12 +30,12 @@ python -c 'import matplotlib' echo "Will use $BLUE'$EDITOR'$NOR to edit files when necessary" -echo -n "PREV_RELEASE (X.y.z) [$PREV_RELEASE]: " -read input -PREV_RELEASE=${input:-$PREV_RELEASE} -echo -n "MILESTONE (X.y) [$MILESTONE]: " -read input -MILESTONE=${input:-$MILESTONE} +# echo -n "PREV_RELEASE (X.y.z) [$PREV_RELEASE]: " +# read input +# PREV_RELEASE=${input:-$PREV_RELEASE} +# echo -n "MILESTONE (X.y) [$MILESTONE]: " +# read input +# MILESTONE=${input:-$MILESTONE} echo -n "VERSION (X.y.z) [$VERSION]:" read input VERSION=${input:-$VERSION} @@ -90,44 +90,44 @@ then read fi -if ask_section "Gen Stats, and authors" -then - - echo - echo $BLUE"here are all the authors that contributed to this release:"$NOR - git log --format="%aN <%aE>" $PREV_RELEASE... | sort -u -f - - echo - echo $BLUE"If you see any duplicates cancel (Ctrl-C), then edit .mailmap." - echo $GREEN"Press enter to continue:"$NOR - read - - echo $BLUE"generating stats"$NOR - python tools/github_stats.py --milestone $MILESTONE > stats.rst - - echo $BLUE"stats.rst files generated."$NOR - echo $GREEN"Please merge it with the right file (github-stats-X.rst) and commit."$NOR - echo $GREEN"press enter to continue."$NOR - read - -fi - -if ask_section "Generate API difference (using frapuccino)" -then - echo $BLUE"Checking out $PREV_RELEASE"$NOR - git checkout tags/$PREV_RELEASE - sleep 1 - echo $BLUE"Saving API to file $PREV_RELEASE"$NOR - frappuccino IPython IPython.kernel IPython.lib IPython.qt IPython.lib.kernel IPython.html IPython.frontend IPython.external --save IPython-$PREV_RELEASE.json - echo $BLUE"coming back to $BRANCH"$NOR - git switch $BRANCH - sleep 1 - echo $BLUE"comparing ..."$NOR - frappuccino IPython IPython.kernel IPython.lib --compare IPython-$PREV_RELEASE.json - echo $GREEN"Use the above guideline to write an API changelog ..."$NOR - echo $GREEN"Press any keys to continue"$NOR - read -fi +# if ask_section "Gen Stats, and authors" +# then +# +# echo +# echo $BLUE"here are all the authors that contributed to this release:"$NOR +# git log --format="%aN <%aE>" $PREV_RELEASE... | sort -u -f +# +# echo +# echo $BLUE"If you see any duplicates cancel (Ctrl-C), then edit .mailmap." +# echo $GREEN"Press enter to continue:"$NOR +# read +# +# echo $BLUE"generating stats"$NOR +# python tools/github_stats.py --milestone $MILESTONE > stats.rst +# +# echo $BLUE"stats.rst files generated."$NOR +# echo $GREEN"Please merge it with the right file (github-stats-X.rst) and commit."$NOR +# echo $GREEN"press enter to continue."$NOR +# read +# +# fi + +# if ask_section "Generate API difference (using frapuccino)" +# then +# echo $BLUE"Checking out $PREV_RELEASE"$NOR +# git checkout tags/$PREV_RELEASE +# sleep 1 +# echo $BLUE"Saving API to file $PREV_RELEASE"$NOR +# frappuccino IPython IPython.kernel IPython.lib IPython.qt IPython.lib.kernel IPython.html IPython.frontend IPython.external --save IPython-$PREV_RELEASE.json +# echo $BLUE"coming back to $BRANCH"$NOR +# git switch $BRANCH +# sleep 1 +# echo $BLUE"comparing ..."$NOR +# frappuccino IPython IPython.kernel IPython.lib --compare IPython-$PREV_RELEASE.json +# echo $GREEN"Use the above guideline to write an API changelog ..."$NOR +# echo $GREEN"Press any keys to continue"$NOR +# read +# fi echo "Cleaning repository" git clean -xfdi @@ -231,7 +231,7 @@ then echo echo $BLUE"Attempting to build package..."$NOR - tools/release + tools/build_release echo $RED'$ shasum -a 256 dist/*' @@ -245,7 +245,7 @@ then echo echo $BLUE"Attempting to build package..."$NOR - tools/release + tools/build_release echo $RED"Check the shasum for SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" echo $RED'$ shasum -a 256 dist/*' @@ -254,6 +254,6 @@ then if ask_section "upload packages ?" then - tools/release upload + twine upload --verbose dist/*.tar.gz dist/*.whl fi fi