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

Refactor interpreter evaluation #137

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

Kyle-Verhoog
Copy link
Member

@Kyle-Verhoog Kyle-Verhoog commented May 15, 2021

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.

@Kyle-Verhoog Kyle-Verhoog requested a review from a team as a code owner May 15, 2021 19:39
@Kyle-Verhoog Kyle-Verhoog force-pushed the interp-lookup branch 5 times, most recently from 1252184 to a6fa6a2 Compare May 16, 2021 05:37
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).
Copy link
Contributor

@jd jd left a 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:
Copy link
Contributor

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
Comment on lines 25 to 26
interp = Interpreter.parse(value)
return interp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interp = Interpreter.parse(value)
return interp
return Interpreter.parse(value)

but ok 😁

Comment on lines +82 to +85
def __hash__(self) -> int:
"""Return the hash of this interpreter."""
return hash(self._path)
Copy link
Contributor

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.

Comment on lines +86 to +89
def __repr__(self) -> str:
"""Return the repr containing the path and version of this interpreter."""
return f"{self.__class__.__name__}('{self.version}', '{self._path}')"
Copy link
Contributor

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?

Comment on lines 90 to 93
def __str__(self) -> str:
"""Return the path of the interpreter executable."""
"""Return the repr of this interpreter."""
return repr(self)
Copy link
Contributor

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.

Comment on lines +117 to +119
FIXME: unfortunately Version will set undefined segments to 0 so
we cannot distinguish between "3.0.0" and "3".
Copy link
Contributor

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)
Copy link
Contributor

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
Comment on lines 145 to 147
if not os.path.isdir(d):
continue
for f in os.listdir(d):
Copy link
Contributor

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:

Suggested change
if not os.path.isdir(d):
continue
for f in os.listdir(d):
try:
for f in os.listdir(d):
[...]
except NotADirectoryError:
continue

Comment on lines +170 to +174
interps = [i for i in cls.find() if i.matches(v)]
if len(interps):
return interps[0]
raise FileNotFoundError("No interpreter matching %r", v)
Copy link
Contributor

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:

Suggested change
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
Comment on lines 191 to 192
except InvalidVersion as e:
raise FileNotFoundError("No interpreter found for %r" % s) from e
Copy link
Contributor

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.

CmdFailure was overloaded to handle two different error cases which was
confusing.
Print out the venv explicitly saying that the specified interpreter is
missing.
@@ -28,6 +28,7 @@
install_requires=[
"dataclasses; python_version<'3.7'",
"click>=7,<8",
"packaging",
Copy link
Member

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
Copy link
Member

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.

riot/riot/riot.py

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()

Comment on lines +95 to +97
def _matches(v1: Version, v2: Version) -> bool:
"""Return if v2 matches v1.
Copy link
Member

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.

https://github.com/pypa/packaging/blob/47d40f640fddb7c97b01315419b6a1421d2dedbb/packaging/version.py#L292-L299

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""A null interpreter means that it was not found."""
# A null interpreter means that it was not found.

?

Comment on lines +487 to +489
pkgs: t.Dict[str, str] = {
name: version for name, version in inst.pkgs if version is not None
}
Copy link
Member

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.

Comment on lines +624 to +626
pkgs_str = " ".join(
f"'{get_pep_dep(name, version)}'" for name, version in inst.pkgs
)
Copy link
Member

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}"
Copy link
Member

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}")
Copy link
Member

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
Copy link
Member

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants