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

Add type hints to variables.py and platforms.py #1042

Merged
merged 4 commits into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ check_untyped_defs = False

no_implicit_optional = True

warn_redundant_casts = True
warn_unused_configs = True
# TODO: enable `warn_unused_ignores = True` once we drop Python 2. Otherwise, we need it because some
# ignores depend on which interpreter we use.
# TODO: enable `warn_unused_ignores` and `warn_reudandant_casts` once we drop Python 2. Otherwise,
# we need it because some ignores depend on which interpreter we use.
warn_redundant_casts = False
warn_unused_ignores = False
warn_no_return = True
warn_return_any = True
Expand Down
9 changes: 6 additions & 3 deletions pex/dist_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@
import email

from pex.third_party.packaging.specifiers import SpecifierSet
from pex.third_party.pkg_resources import DistInfoDistribution
from pex.third_party.pkg_resources import DistInfoDistribution, Distribution
from pex.typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Optional


def requires_python(dist):
# type: (Distribution) -> Optional[SpecifierSet]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that MyPy doesn't actually know what these are due to the vendoring. But I believe that it will at least check we are using type A, rather than type Z; it just doesn't understand the methods/fields on the type.

And this helpful for the reader.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can possibly create stubs for these if needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, that'd be interesting as a followup once we have higher coverage. pkg_resources is pretty key to Pex. Having type hints working with Pex would be great.

"""Examines dist for `Python-Requires` metadata and returns version constraints if any.

See: https://www.python.org/dev/peps/pep-0345/#requires-python

:param dist: A distribution to check for `Python-Requires` metadata.
:type dist: :class:`pkg_resources.Distribution`
:return: The required python version specifiers.
:rtype: :class:`packaging.specifiers.SpecifierSet` or None
"""
if not dist.has_metadata(DistInfoDistribution.PKG_INFO):
return None
Expand Down
15 changes: 10 additions & 5 deletions pex/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from pex.variables import ENV

if TYPE_CHECKING:
from typing import Dict
from typing import Dict, Iterator


class PythonIdentity(object):
Expand Down Expand Up @@ -144,6 +144,7 @@ def version(self):

@property
def version_str(self):
# type: () -> str
return ".".join(map(str, self.version))

@property
Expand All @@ -164,14 +165,13 @@ def requirement(self):

@property
def distribution(self):
# type: () -> Distribution
return Distribution(project_name=self.interpreter, version=self.version_str)

def iter_supported_platforms(self):
# type: () -> Iterator[Platform]
"""All platforms supported by the associated interpreter ordered from most specific to
least.

:rtype: iterator of :class:`Platform`
"""
least."""
for tags in self._supported_tags:
yield Platform.from_tags(platform=tags.platform, python=tags.interpreter, abi=tags.abi)

Expand Down Expand Up @@ -200,6 +200,7 @@ def matches(self, requirement):
return self.distribution in requirement

def hashbang(self):
# type: () -> str
hashbang_string = self.INTERPRETER_NAME_TO_HASHBANG.get(
self._interpreter_name, "CPython"
) % {
Expand All @@ -211,11 +212,13 @@ def hashbang(self):

@property
def python(self):
# type: () -> str
# return the python version in the format of the 'python' key for distributions
# specifically, '2.7', '3.2', etc.
return "%d.%d" % (self.version[0:2])

def __str__(self):
# type: () -> str
# N.B.: Kept as distinct from __repr__ to support legacy str(identity) used by Pants v1 when
# forming cache locations.
return "{interpreter_name}-{major}.{minor}.{patch}".format(
Expand All @@ -226,6 +229,7 @@ def __str__(self):
)

def __repr__(self):
# type: () -> str
return (
"{type}({binary!r}, {python_tag!r}, {abi_tag!r}, {platform_tag!r}, {version!r})".format(
type=self.__class__.__name__,
Expand All @@ -246,6 +250,7 @@ def __eq__(self, other):
return self._tup() == other._tup()

def __hash__(self):
# type: () -> int
return hash(self._tup())


Expand Down
14 changes: 9 additions & 5 deletions pex/interpreter_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
import os

from pex.common import die
from pex.interpreter import PythonIdentity
from pex.interpreter import PythonIdentity, PythonInterpreter
from pex.typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Iterable, Optional, Tuple


def validate_constraints(constraints):
# type: (Iterable[str]) -> None
# TODO: add check to see if constraints are mutually exclusive (bad) so no time is wasted:
# https://github.com/pantsbuild/pex/issues/432
for req in constraints:
Expand All @@ -26,25 +31,24 @@ class UnsatisfiableInterpreterConstraintsError(Exception):
"""Indicates interpreter constraints could not be satisfied."""

def __init__(self, constraints, candidates, failures):
# type: (Iterable[str], Iterable[PythonInterpreter], Iterable[Tuple[str, str]]) -> None
"""
:param constraints: The constraints that could not be satisfied.
:type constraints: iterable of str
:param candidates: The python interpreters that were compared against the constraints.
:type candidates: iterable of :class:`pex.interpreter.PythonInterpreter`
"""
self.constraints = tuple(constraints)
self.candidates = tuple(candidates)
self.failures = tuple(failures)
super(UnsatisfiableInterpreterConstraintsError, self).__init__(self.create_message())

def create_message(self, preamble=None):
# type: (Optional[str]) -> str
"""Create a message describing failure to find matching interpreters with an optional
preamble.

:param str preamble: An optional preamble to the message that will be displayed above it
:param preamble: An optional preamble to the message that will be displayed above it
separated by an empty blank line.
:return: A descriptive message useable for display to an end user.
:rtype: str
"""
preamble = "{}\n\n".format(preamble) if preamble else ""

Expand Down
10 changes: 9 additions & 1 deletion pex/pex_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@

import warnings

from pex.typing import TYPE_CHECKING

if TYPE_CHECKING:
from pex.pex_info import PexInfo
from pex.variables import Variables


class PEXWarning(Warning):
"""Indicates a warning from PEX about suspect buildtime or runtime configuration."""


def configure_warnings(pex_info, env):
if env.PEX_VERBOSE > 0:
# type: (PexInfo, Variables) -> None
if env.PEX_VERBOSE is not None and env.PEX_VERBOSE > 0:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a bug, although I don't know if we ever hit it. It's possible for PEX_VERBOSE to be None, such as if you call ENV.strip() and the user did not define the value.

Copy link
Member

Choose a reason for hiding this comment

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

There is a bug lurking but its unhittable:
https://github.com/pantsbuild/pex/blob/aae2a6ad55e148ada43f36c1d486af9b2312af50/pex/variables.py#L364-L372
https://github.com/pantsbuild/pex/blob/aae2a6ad55e148ada43f36c1d486af9b2312af50/pex/variables.py#L104-L113

... combined with use_defaults is True for the global ENV which is what the only use of configure_warnings in production passes (all of production only uses the global ENV).

emit_warnings = True
elif env.PEX_EMIT_WARNINGS is not None:
emit_warnings = env.PEX_EMIT_WARNINGS
Expand All @@ -24,4 +31,5 @@ def configure_warnings(pex_info, env):


def warn(message):
# type: (str) -> None
warnings.warn(message, category=PEXWarning, stacklevel=2)
50 changes: 40 additions & 10 deletions pex/platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from collections import namedtuple
from textwrap import dedent

from pex.typing import TYPE_CHECKING, cast

if TYPE_CHECKING:
from typing import Union


# TODO(#1041): Use typing.NamedTuple once we require Python 3.
class Platform(namedtuple("Platform", ["platform", "impl", "version", "abi"])):
"""Represents a target platform and it's extended interpreter compatibility tags (e.g.
implementation, version and ABI)."""
Expand All @@ -18,6 +24,7 @@ class InvalidPlatformError(Exception):

@classmethod
def create(cls, platform):
# type: (Union[str, Platform]) -> Platform
if isinstance(platform, Platform):
return platform

Expand Down Expand Up @@ -62,33 +69,56 @@ def create(cls, platform):

@staticmethod
def _maybe_prefix_abi(impl, version, abi):
# type: (str, str, str) -> str
if impl != "cp":
return abi
# N.B. This permits CPython users to pass in simpler extended platform
# strings like `linux-x86_64-cp-27-mu` vs e.g. `linux-x86_64-cp-27-cp27mu`.
impl_ver = "".join((impl, version))
return abi if abi.startswith(impl_ver) else "".join((impl_ver, abi))

@classmethod
def from_tags(cls, platform, python, abi):
# type: (str, str, str) -> Platform
"""Creates a platform corresponding to wheel compatibility tags.

See: https://www.python.org/dev/peps/pep-0425/#details
"""
impl, version = python[:2], python[2:]
return cls(platform=platform, impl=impl, version=version, abi=abi)

def __new__(cls, platform, impl, version, abi):
if not all((platform, impl, version, abi)):
raise cls.InvalidPlatformError(
"Platform specifiers cannot have blank fields. Given platform={platform!r}, impl={impl!r}, "
"version={version!r}, abi={abi!r}".format(
"Platform specifiers cannot have blank fields. Given platform={platform!r}, "
"impl={impl!r}, version={version!r}, abi={abi!r}".format(
platform=platform, impl=impl, version=version, abi=abi
)
)
platform = platform.replace("-", "_").replace(".", "_")
abi = cls._maybe_prefix_abi(impl, version, abi)
return super(Platform, cls).__new__(cls, platform, impl, version, abi)

@classmethod
def from_tags(cls, platform, python, abi):
"""Creates a platform corresponding to wheel compatibility tags.
@property
def platform(self):
# type: () -> str
return cast(str, super(Platform, self).platform)

See: https://www.python.org/dev/peps/pep-0425/#details
"""
impl, version = python[:2], python[2:]
return cls(platform=platform, impl=impl, version=version, abi=abi)
@property
def impl(self):
# type: () -> str
return cast(str, super(Platform, self).impl)

@property
def version(self):
# type: () -> str
return cast(str, super(Platform, self).version)

@property
def abi(self):
# type: () -> str
return cast(str, super(Platform, self).abi)

def __str__(self):
return self.SEP.join(self)
# type: () -> str
return cast(str, self.SEP.join(self))
3 changes: 1 addition & 2 deletions pex/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,7 @@ def write_simple_pex(
return pb


# We would normally use a dataclass or NamedTuple, but can't do that with Python 2 in a way
# understood by MyPy.
# TODO(#1041): use `typing.NamedTuple` once we require Python 3.
class IntegResults(object):
"""Convenience object to return integration run results."""

Expand Down
Loading