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
Support sys.pycache_prefix on py38 #5864
Support sys.pycache_prefix on py38 #5864
Conversation
d9f1026
to
b456eb2
Compare
2b8d203
to
7c98874
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neat
@@ -102,7 +104,7 @@ def create_module(self, spec): | |||
return None # default behaviour is fine | |||
|
|||
def exec_module(self, module): | |||
fn = module.__spec__.origin | |||
fn = Path(module.__spec__.origin) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol, in my last patch I moved this away from pathlib due to needing fspath
hax -- this is fine though I guess 🤷♂
@@ -291,12 +294,12 @@ def _read_pyc(source, pyc, trace=lambda x: None): | |||
Return rewritten code if successful or None if not. | |||
""" | |||
try: | |||
fp = open(pyc, "rb") | |||
fp = open(fspath(pyc), "rb") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one shouldn't need fspath
should it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, on py35
open
doesn't support Path
objects, and there are a bunch of tests which call _read_pyc
directly to handle different situations correctly. I think it is fine to keep it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we need hax for py35 to use pathlib I'd prefer we don't use pathlib until we drop python3.5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it is hack to be honest... it is a compatibility function, like we have many others in compat
. It is literally a if/else with an implementation specific to py35.
This request would need me to review the entire patch, and I really would like to avoid that unless we have good reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that this particular case is just a compat function, but pathlib in general feels like a liability if it's going to blow up mysteriously in python3.5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if it's going to blow up mysteriously in python3.5
Definitely agree for external APIs, but this is an internal change. And it doesn't blow mysteriously: remove that call and it does fail on python3.5 immediately, with a good error message. Or perhaps you mean something else by mysteriously?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this particular usage doesn't, but I'm not confident of test coverage about all of our uses of such functions
there's also potentially more surface area than expected here since it's an import hook -- but maybe it's fine
I just have my reservations about pathlib being a good idea when there's some pretty easy footguns :) (and I don't like pathlib myself)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I disagree, so let's hold this PR until we drop py35 then. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with this PR btw, just concerned is all ;) -- I think if you use exist_ok=True
below I'll press approve :D
src/_pytest/assertion/rewrite.py
Outdated
@@ -1021,9 +1024,9 @@ def visit_Compare(self, comp: ast.Compare): | |||
def try_mkdir(cache_dir): | |||
"""Attempts to create the given directory, returns True if successful""" | |||
try: | |||
os.mkdir(cache_dir) | |||
os.makedirs(fspath(cache_dir)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exist_ok=True
and then I think you can avoid the except:
block maybe -- or just use suppress
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed to use cache_dir.mkdir
as you suggest, unfortunately I could not get rid of FileExistsError
handling (got some test failures), plus test_try_mkdir
seems to be fine tuned already and I would have to change it now that cache_dir
would be a Path
, so I would prefer to not really change this if possible. 😬
I also tried to use suppress
, but it loses its appeal because we still need try/except for the other errors (so we can return False
on some of them):
diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py
index 415676aaa..0527a3639 100644
--- a/src/_pytest/assertion/rewrite.py
+++ b/src/_pytest/assertion/rewrite.py
@@ -1,5 +1,6 @@
"""Rewrite assertion AST to produce nice error messages"""
import ast
+import contextlib
import errno
import functools
import importlib.abc
@@ -1024,8 +1025,8 @@ warn_explicit(
def try_mkdir(cache_dir):
"""Attempts to create the given directory, returns True if successful"""
try:
- os.makedirs(fspath(cache_dir))
- except FileExistsError:
+ with contextlib.suppress(FileExistsError):
+ os.makedirs(fspath(cache_dir))
# Either the pycache directory already exists (the
# common case) or it's blocked by a non-dir node. In the
# latter case, we'll ignore it in _write_pyc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean to use makedirs(..., exist_ok=True)
~essentially mkdir -p
but only available in python3 :)
# path = '/home/user/proj/test_app.py' | ||
# we want: | ||
# '/tmp/pycs/home/user/proj' | ||
return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems broken for windows if I had to guess
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? If you see the unittest, it seems correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh I see, it's using .parts
-- thought this was slicing strings -- I'll have to look into this more :/
"""os.fspath replacement, useful to point out when we should replace it by the | ||
real function once we drop py35. | ||
""" | ||
return str(p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not ~quite right -- but maybe good enough for our uses? here's the implementation from python3.6:
def _fspath(path):
"""Return the path representation of a path-like object.
If str or bytes is passed in, it is returned unchanged. Otherwise the
os.PathLike interface is used to get the path representation. If the
path representation is not str or bytes, TypeError is raised. If the
provided path is not str, bytes, or os.PathLike, TypeError is raised.
"""
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path_repr = path_type.__fspath__(path)
except AttributeError:
if hasattr(path_type, '__fspath__'):
raise
else:
raise TypeError("expected str, bytes or os.PathLike object, "
"not " + path_type.__name__)
if isinstance(path_repr, (str, bytes)):
return path_repr
else:
raise TypeError("expected {}.__fspath__() to return str or bytes, "
"not {}".format(path_type.__name__,
type(path_repr).__name__))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but maybe good enough for our uses?
I think so. We will be using esporadically to convert Path
objects only, and __fspath__
of Path
objects implement a call to str()
:
def __fspath__(self):
return str(self)
And it is also cached:
def __str__(self):
"""Return the string representation of the path, suitable for
passing to system calls."""
try:
return self._str
except AttributeError:
self._str = self._format_parsed_parts(self._drv, self._root,
self._parts) or '.'
return self._str
So I think it is fine as is. Also we will be dropping py35 soon I think, so we can just call os.fspath
directly. 👍
@@ -291,12 +294,12 @@ def _read_pyc(source, pyc, trace=lambda x: None): | |||
Return rewritten code if successful or None if not. | |||
""" | |||
try: | |||
fp = open(pyc, "rb") | |||
fp = open(fspath(pyc), "rb") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, on py35
open
doesn't support Path
objects, and there are a bunch of tests which call _read_pyc
directly to handle different situations correctly. I think it is fine to keep it.
# path = '/home/user/proj/test_app.py' | ||
# we want: | ||
# '/tmp/pycs/home/user/proj' | ||
return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? If you see the unittest, it seems correct.
"""os.fspath replacement, useful to point out when we should replace it by the | ||
real function once we drop py35. | ||
""" | ||
return str(p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but maybe good enough for our uses?
I think so. We will be using esporadically to convert Path
objects only, and __fspath__
of Path
objects implement a call to str()
:
def __fspath__(self):
return str(self)
And it is also cached:
def __str__(self):
"""Return the string representation of the path, suitable for
passing to system calls."""
try:
return self._str
except AttributeError:
self._str = self._format_parsed_parts(self._drv, self._root,
self._parts) or '.'
return self._str
So I think it is fine as is. Also we will be dropping py35 soon I think, so we can just call os.fspath
directly. 👍
@pytest.mark.parametrize( | ||
"prefix, source, expected", | ||
[ | ||
("c:/tmp/pycs", "d:/projects/src/foo.py", "c:/tmp/pycs/projects/src"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asottile here are the tests for Windows, which I obtained by testing how sys.pycache_prefix
worked on my system.
src/_pytest/assertion/rewrite.py
Outdated
@@ -1021,9 +1024,9 @@ def visit_Compare(self, comp: ast.Compare): | |||
def try_mkdir(cache_dir): | |||
"""Attempts to create the given directory, returns True if successful""" | |||
try: | |||
os.mkdir(cache_dir) | |||
os.makedirs(fspath(cache_dir)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed to use cache_dir.mkdir
as you suggest, unfortunately I could not get rid of FileExistsError
handling (got some test failures), plus test_try_mkdir
seems to be fine tuned already and I would have to change it now that cache_dir
would be a Path
, so I would prefer to not really change this if possible. 😬
I also tried to use suppress
, but it loses its appeal because we still need try/except for the other errors (so we can return False
on some of them):
diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py
index 415676aaa..0527a3639 100644
--- a/src/_pytest/assertion/rewrite.py
+++ b/src/_pytest/assertion/rewrite.py
@@ -1,5 +1,6 @@
"""Rewrite assertion AST to produce nice error messages"""
import ast
+import contextlib
import errno
import functools
import importlib.abc
@@ -1024,8 +1025,8 @@ warn_explicit(
def try_mkdir(cache_dir):
"""Attempts to create the given directory, returns True if successful"""
try:
- os.makedirs(fspath(cache_dir))
- except FileExistsError:
+ with contextlib.suppress(FileExistsError):
+ os.makedirs(fspath(cache_dir))
# Either the pycache directory already exists (the
# common case) or it's blocked by a non-dir node. In the
# latter case, we'll ignore it in _write_pyc.
Actually someone brought to my attention that py35 support ends in late 2020, not late 2019 as I thought initially. Regarding this PR, should we:
I myself vote for 1), |
yeah I'm ok to go ahead with this -- just want to see |
7c98874
to
8707521
Compare
Done in 8707521. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8707521
to
6d42284
Compare
6d42284
to
6f20b4b
Compare
Fix #4730