-
Notifications
You must be signed in to change notification settings - Fork 15
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
Refactor interpreter evaluation #137
base: master
Are you sure you want to change the base?
Conversation
1252184
to
a6fa6a2
Compare
Before, interpreter evaluation was done at run-time when the interpreter was first used. With this patch interpreters are looked up when the environments are generated. Also, more precise interpreter specification is possible. Before, specifying "3" in a riotfile would result in a "python3" executable being looked up. So riot would fail if Python was installed as "python" and no "python3" executable was available on the path. Now interpreters can be specified with a file path, executable name or a version (which will be looked up on the PATH).
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 like the overall direction :)
@@ -1,9 +1,6 @@ | |||
name: CI | |||
on: | |||
pull_request: |
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.
Sounds unrelated.
riot/cli.py
Outdated
interp = Interpreter.parse(value) | ||
return interp |
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.
interp = Interpreter.parse(value) | |
return interp | |
return Interpreter.parse(value) |
but ok 😁
def __hash__(self) -> int: | ||
"""Return the hash of this interpreter.""" | ||
return hash(self._path) |
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.
Don't do this, just pass hash=False
to _version
L72.
def __repr__(self) -> str: | ||
"""Return the repr containing the path and version of this interpreter.""" | ||
return f"{self.__class__.__name__}('{self.version}', '{self._path}')" |
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.
Is that really better than dataclass generated method?
def __str__(self) -> str: | ||
"""Return the path of the interpreter executable.""" | ||
"""Return the repr of this interpreter.""" | ||
return repr(self) |
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.
ditto, not worth it I guess with dataclass doing the job.
FIXME: unfortunately Version will set undefined segments to 0 so | ||
we cannot distinguish between "3.0.0" and "3". |
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's a problem, in general, and especially in our case.
riot/riot.py
Outdated
interp_paths: t.List[str] = [] | ||
# packaging says that the regular expression needs to be compiled | ||
# with the following flags | ||
expr = re.compile(f"python{VERSION_PATTERN}$", re.IGNORECASE | re.VERBOSE) |
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.
define this has a class variable to compile once and for all
riot/riot.py
Outdated
if not os.path.isdir(d): | ||
continue | ||
for f in os.listdir(d): |
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.
As a rule of thumb, never do that. The pattern of calling stat
before doing an I/O is at best unoptimized, at worst a security issue.
You should always do a single file operation and catch errors, like this:
if not os.path.isdir(d): | |
continue | |
for f in os.listdir(d): | |
try: | |
for f in os.listdir(d): | |
[...] | |
except NotADirectoryError: | |
continue |
interps = [i for i in cls.find() if i.matches(v)] | ||
if len(interps): | ||
return interps[0] | ||
raise FileNotFoundError("No interpreter matching %r", v) |
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.
There's no need to build the entire list. If it huges, it'll take hours for nothing.
You could use first
which I like for this: https://pypi.org/project/first/
Otherwise:
interps = [i for i in cls.find() if i.matches(v)] | |
if len(interps): | |
return interps[0] | |
raise FileNotFoundError("No interpreter matching %r", v) | |
for interp in cls.find(): | |
if interp.matches(v): | |
return interp | |
raise FileNotFoundError("No interpreter matching %r", v) |
riot/riot.py
Outdated
except InvalidVersion as e: | ||
raise FileNotFoundError("No interpreter found for %r" % s) from e |
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 wouldn't change that exception, it does not seem fair.
A bit more clear that the "version" is not necessarily a version.
CmdFailure was overloaded to handle two different error cases which was confusing.
Print out the venv explicitly saying that the specified interpreter is missing.
a6fa6a2
to
6724866
Compare
@@ -28,6 +28,7 @@ | |||
install_requires=[ | |||
"dataclasses; python_version<'3.7'", | |||
"click>=7,<8", | |||
"packaging", |
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.
is there a min version we need?
def __post_init__(self): | ||
"""Get the version of the interpreter.""" | ||
output = subprocess.check_output( | ||
[self._path, "--version"], stderr=subprocess.STDOUT |
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.
you might want to rebase on master
we made some changes to how we get version from Python to alleviate the parsing of --version
output.
Lines 81 to 91 in 6d064ca
def version(self) -> str: | |
path = self.path() | |
output = subprocess.check_output( | |
[ | |
path, | |
"-c", | |
'import sys; print("%s.%s.%s" % (sys.version_info.major, sys.version_info.minor, sys.version_info.micro))', | |
], | |
) | |
return output.decode().strip() |
def _matches(v1: Version, v2: Version) -> bool: | ||
"""Return if v2 matches v1. |
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.
The better approach is to have a custom Version
class that inherits from packaging.version.Version
and which defines a custom Version._key
.
Seems like we want to compare versions but ignoring pre/post/dev/local ?
which if that is the case, then a custom key, and then relying on the built-in comparisons to Version
should work.
"""Return whether the given version matches the version of the interpreter.""" | ||
return self._matches(self.version, version) | ||
|
||
def path(self) -> str: |
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.
def path(self) -> str: | |
@property | |
def path(self) -> str: |
?
@@ -233,20 +346,26 @@ class VenvInstance: | |||
env: t.Tuple[t.Tuple[str, str]] | |||
name: t.Optional[str] | |||
pkgs: t.Tuple[t.Tuple[str, str]] | |||
py: Interpreter | |||
interpreter_hint: str | |||
"""A null interpreter means that it was not found.""" |
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.
"""A null interpreter means that it was not found.""" | |
# A null interpreter means that it was not found. |
?
pkgs: t.Dict[str, str] = { | ||
name: version for name, version in inst.pkgs if version is not None | ||
} |
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 found transforming/relocating this and pkg_str
into VenvInstance
much nicer to work with.
pkgs_str = " ".join( | ||
f"'{get_pep_dep(name, version)}'" for name, version in inst.pkgs | ||
) |
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.
relocating this to VenvInstance
will definitely be nice since it seems like we do this transformation in at least 3 places.
) | ||
env_str = env_to_str(inst.env) | ||
click.echo( | ||
f"{inst.name} {env_str} <Interpreter missing for '{inst.interpreter_hint}'> {pkgs_str}" |
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.
should the str/repr output of Interpreter
just state whether it exists or not?
pkgs_str = " ".join( | ||
f"'{get_pep_dep(name, version)}'" for name, version in inst.pkgs | ||
) | ||
env_str = env_to_str(inst.env) | ||
py_str = f"Python {inst.py}" | ||
click.echo(f"{inst.name} {env_str} {py_str} {pkgs_str}") | ||
click.echo(f"{inst.name} {env_str} {inst.interpreter} {pkgs_str}") |
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.
should we make VenvInstance.__str__
print what we expect here?
or else a helper method would help since we have this format in at least 2 places
|
||
for inst in self.venv.instances(pattern=pattern): | ||
if inst.interpreter is None: | ||
continue |
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.
raise?
wondering how we can end up in this state other than misconfiguration?
Before, interpreter evaluation was done at run-time when the interpreter
was first used.
With this patch interpreters are looked up when the environments are
generated.
Also more precise interpreter specification is possible. Before,
specifying "3" in a riotfile would result in a "python3" executable
being looked up. So riot would fail if Python was installed as "python"
and no "python3" executable was available on the path.
Now the path is searched for python executables which are then
referenced when resolving the python to use for an environment.