Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

convert magics/{code,basic}.py and oinspect.py to pathlib #13783

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
11 changes: 4 additions & 7 deletions IPython/core/magics/basic.py
Expand Up @@ -3,7 +3,7 @@

from logging import error
import io
import os
from pathlib import Path
from pprint import pformat
import sys
from warnings import warn
Expand Down Expand Up @@ -568,10 +568,7 @@ def precision(self, s=''):
return ptformatter.float_format

@magic_arguments.magic_arguments()
@magic_arguments.argument(
'filename', type=str,
help='Notebook name or filename'
)
@magic_arguments.argument("filename", type=Path, help="Notebook name or filename")
@line_magic
def notebook(self, s):
"""Export and convert IPython notebooks.
Expand All @@ -580,7 +577,7 @@ def notebook(self, s):
For example, to export the history to "foo.ipynb" do "%notebook foo.ipynb".
"""
args = magic_arguments.parse_argstring(self.notebook, s)
outfname = os.path.expanduser(args.filename)
outfname = args.filename.expanduser()

from nbformat import write, v4

Expand All @@ -594,7 +591,7 @@ def notebook(self, s):
source=source
))
nb = v4.new_notebook(cells=cells)
with io.open(outfname, "w", encoding="utf-8") as f:
with outfname.open("w", encoding="utf-8") as f:
write(nb, f, version=4)

@magics_class
Expand Down
60 changes: 31 additions & 29 deletions IPython/core/magics/code.py
Expand Up @@ -15,7 +15,6 @@
# Stdlib
import inspect
import io
import os
import re
import sys
import ast
Expand Down Expand Up @@ -219,11 +218,11 @@ def save(self, parameter_s=''):
append = 'a' in opts
mode = 'a' if append else 'w'
ext = '.ipy' if raw else '.py'
fname, codefrom = args[0], " ".join(args[1:])
if not fname.endswith(('.py','.ipy')):
fname += ext
fname = os.path.expanduser(fname)
file_exists = os.path.isfile(fname)
fname, codefrom = Path(args[0]), " ".join(args[1:])
if fname.suffix not in (".py", ".ipy"):
fname = fname.with_suffix(fname.suffix + ext)
fname = fname.expanduser()
file_exists = fname.is_file()
if file_exists and not force and not append:
try:
overwrite = self.shell.ask_yes_no('File `%s` exists. Overwrite (y/[N])? ' % fname, default='n')
Expand All @@ -238,7 +237,7 @@ def save(self, parameter_s=''):
except (TypeError, ValueError) as e:
print(e.args[0])
return
with io.open(fname, mode, encoding="utf-8") as f:
with fname.open(mode, encoding="utf-8") as f:
if not file_exists or not append:
f.write("# coding: utf-8\n")
f.write(cmds)
Expand Down Expand Up @@ -409,12 +408,12 @@ def _find_edit_target(shell, args, opts, last_call):
def make_filename(arg):
"Make a filename from the given args"
try:
filename = get_py_filename(arg)
filename = Path(get_py_filename(arg))
except IOError:
# If it ends with .py but doesn't already exist, assume we want
# a new file.
if arg.endswith('.py'):
filename = arg
filename = Path(arg)
else:
filename = None
return filename
Expand Down Expand Up @@ -474,8 +473,9 @@ class DataIsObject(Exception): pass
# For objects, try to edit the file where they are defined
filename = find_file(data)
if filename:
if 'fakemodule' in filename.lower() and \
inspect.isclass(data):
if "fakemodule" in str(filename).lower() and inspect.isclass(
data
):
# class created by %edit? Try to find source
# by looking for method definitions instead, the
# __module__ in those classes is FakeModule.
Expand All @@ -484,14 +484,16 @@ class DataIsObject(Exception): pass
if not inspect.ismethod(attr):
continue
filename = find_file(attr)
if filename and \
'fakemodule' not in filename.lower():
if (
filename
and "fakemodule" not in str(filename).lower()
):
# change the attribute to be the edit
# target instead
data = attr
break

m = ipython_input_pat.match(os.path.basename(filename))
m = ipython_input_pat.match(filename.name)
if m:
raise InteractivelyDefined(int(m.groups()[0])) from e

Expand All @@ -517,7 +519,7 @@ class DataIsObject(Exception): pass
use_temp = False

if use_temp:
filename = shell.mktempfile(data)
filename = Path(shell.mktempfile(data))
print('IPython will make a temporary file named:',filename)

# use last_call to remember the state of the previous call, but don't
Expand All @@ -534,11 +536,11 @@ class DataIsObject(Exception): pass

def _edit_macro(self,mname,macro):
"""open an editor with the macro data in a file"""
filename = self.shell.mktempfile(macro.value)
self.shell.hooks.editor(filename)
filename = Path(self.shell.mktempfile(macro.value))
self.shell.hooks.editor(str(filename))

# and make a new macro object, to replace the old one
mvalue = Path(filename).read_text(encoding="utf-8")
mvalue = filename.read_text(encoding="utf-8")
self.shell.user_ns[mname] = Macro(mvalue)

@skip_doctest
Expand Down Expand Up @@ -705,19 +707,18 @@ def edit(self, parameter_s='',last_call=['','']):
return

if is_temp:
self._knowntemps.add(filename)
elif (filename in self._knowntemps):
self._knowntemps.add(str(filename))
elif str(filename) in self._knowntemps:
is_temp = True


# do actual editing here
print('Editing...', end=' ')
sys.stdout.flush()
filepath = Path(filename)
try:
# Quote filenames that may have spaces in them when opening
# the editor
quoted = filename = str(filepath.absolute())
quoted = str(filename.absolute())
if " " in quoted:
quoted = "'%s'" % quoted
self.shell.hooks.editor(quoted, lineno)
Expand All @@ -728,27 +729,28 @@ def edit(self, parameter_s='',last_call=['','']):
# XXX TODO: should this be generalized for all string vars?
# For now, this is special-cased to blocks created by cpaste
if args.strip() == "pasted_block":
self.shell.user_ns["pasted_block"] = filepath.read_text(encoding="utf-8")
self.shell.user_ns["pasted_block"] = filename.read_text(encoding="utf-8")

if 'x' in opts: # -x prevents actual execution
print()
else:
print('done. Executing edited code...')
with preserve_keys(self.shell.user_ns, '__file__'):
if not is_temp:
self.shell.user_ns["__file__"] = filename
self.shell.user_ns["__file__"] = str(filename.absolute())
if "r" in opts: # Untranslated IPython code
source = filepath.read_text(encoding="utf-8")
source = filename.read_text(encoding="utf-8")
self.shell.run_cell(source, store_history=False)
else:
self.shell.safe_execfile(filename, self.shell.user_ns,
self.shell.user_ns)
self.shell.safe_execfile(
str(filename.absolute()), self.shell.user_ns, self.shell.user_ns
)

if is_temp:
try:
return filepath.read_text(encoding="utf-8")
return filename.read_text(encoding="utf-8")
except IOError as msg:
if Path(msg.filename) == filepath:
if Path(msg.filename) == filename:
warn('File not found. Did you forget to save?')
return
else:
Expand Down
32 changes: 18 additions & 14 deletions IPython/core/oinspect.py
Expand Up @@ -14,6 +14,7 @@
# stdlib modules
from dataclasses import dataclass
from inspect import signature
from pathlib import Path
from textwrap import dedent
import ast
import html
Expand Down Expand Up @@ -197,15 +198,17 @@ def get_encoding(obj):
# filesystem.
if ofile is None:
return None
elif ofile.endswith(('.so', '.dll', '.pyd')):
elif ofile.suffix in (".so", ".dll", ".pyd"):
return None
elif not os.path.isfile(ofile):
elif not ofile.is_file():
return None
else:
# Print only text files, not extension binaries. Note that
# getsourcelines returns lineno with 1-offset and page() uses
# 0-offset, so we must adjust.
with stdlib_io.open(ofile, 'rb') as buffer: # Tweaked to use io.open for Python 2
with stdlib_io.open(
str(ofile), "rb"
) as buffer: # Tweaked to use io.open for Python 2
encoding, _lines = openpy.detect_encoding(buffer.readline)
return encoding

Expand Down Expand Up @@ -390,7 +393,7 @@ def _get_wrapped(obj):
return orig_obj
return obj

def find_file(obj) -> Optional[str]:
def find_file(obj) -> Optional[Path]:
"""Find the absolute path to the file where an object was defined.

This is essentially a robust wrapper around `inspect.getabsfile`.
Expand All @@ -403,7 +406,7 @@ def find_file(obj) -> Optional[str]:

Returns
-------
fname : str
fname : Optional[Path]
The absolute path to the file where the object was defined.
"""
obj = _get_wrapped(obj)
Expand All @@ -422,7 +425,7 @@ def find_file(obj) -> Optional[str]:
except OSError:
pass

return fname
return Path(fname) if fname is not None else None


def find_source_lines(obj):
Expand Down Expand Up @@ -618,10 +621,10 @@ def pfile(self, obj, oname=''):
# filesystem.
if ofile is None:
print("Could not find file for object")
elif ofile.endswith((".so", ".dll", ".pyd")):
print("File %r is binary, not printing." % ofile)
elif not os.path.isfile(ofile):
print('File %r does not exist, not printing.' % ofile)
elif ofile.suffix in (".so", ".dll", ".pyd"):
print("File %r is binary, not printing." % str(ofile))
elif not ofile.is_file():
print("File %r does not exist, not printing." % str(ofile))
else:
# Print only text files, not extension binaries. Note that
# getsourcelines returns lineno with 1-offset and page() uses
Expand Down Expand Up @@ -1040,11 +1043,12 @@ def info(self, obj, oname="", info=None, detail_level=0) -> InfoDict:
# if the file was binary
binary_file = True
else:
if fname.endswith(('.so', '.dll', '.pyd')):
fname_msg = str(fname)
if fname.suffix in (".so", ".dll", ".pyd"):
binary_file = True
elif fname.endswith('<string>'):
fname = 'Dynamically generated function. No source code available.'
out['file'] = compress_user(fname)
elif fname.name.endswith("<string>"):
fname_msg = "Dynamically generated function. No source code available."
out["file"] = compress_user(fname_msg)

# Original source code for a callable, class or property.
if detail_level:
Expand Down
5 changes: 2 additions & 3 deletions IPython/core/tests/test_magic.py
Expand Up @@ -1366,10 +1366,9 @@ def _run_edit_test(arg_s, exp_filename=None,
filename, lineno, is_temp = M._find_edit_target(ip, args, opts, last_call)

if exp_filename is not None:
assert exp_filename == filename
assert Path(exp_filename) == filename
if exp_contents is not None:
with io.open(filename, 'r', encoding='utf-8') as f:
contents = f.read()
contents = filename.read_text(encoding="utf-8")
assert exp_contents == contents
if exp_lineno != -1:
assert exp_lineno == lineno
Expand Down
10 changes: 5 additions & 5 deletions IPython/core/tests/test_oinspect.py
Expand Up @@ -8,7 +8,7 @@
from contextlib import contextmanager
from inspect import signature, Signature, Parameter
import inspect
import os
from pathlib import Path
import pytest
import re
import sys
Expand Down Expand Up @@ -71,15 +71,15 @@ def test_inspect_getfile_raises_exception():
# A couple of utilities to ensure these tests work the same from a source or a
# binary install
def pyfile(fname):
return os.path.normcase(re.sub('.py[co]$', '.py', fname))
return fname.with_suffix(re.sub(r"\.py[co]$", ".py", fname.suffix))


def match_pyfiles(f1, f2):
assert pyfile(f1) == pyfile(f2)


def test_find_file():
match_pyfiles(oinspect.find_file(test_find_file), os.path.abspath(__file__))
match_pyfiles(oinspect.find_file(test_find_file), Path(__file__).absolute())
assert oinspect.find_file(type) is None
assert oinspect.find_file(SourceModuleMainTest) is None
assert oinspect.find_file(SourceModuleMainTest()) is None
Expand All @@ -97,7 +97,7 @@ def wrapper(*a, **kw):
def f(x):
"My docstring"

match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__))
match_pyfiles(oinspect.find_file(f), Path(__file__).absolute())
assert f.__doc__ == "My docstring"


Expand All @@ -113,7 +113,7 @@ def noop2(f, *a, **kw):
def f(x):
"My docstring 2"

match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__))
match_pyfiles(oinspect.find_file(f), Path(__file__).absolute())
assert f.__doc__ == "My docstring 2"


Expand Down