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 annotations via MonkeyType #469

Merged
merged 12 commits into from
Oct 25, 2019
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ venv.bak/
.mypy_cache/
.dmypy.json
dmypy.json
monkeytype.sqlite3
3 changes: 0 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ env:

matrix:
fast_finish: true
allow_failures:
- env:
TOXENV: lint-mypy

include:
- python: 3.7
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ include LICENSE
include README.rst
include AUTHORS
include .coveragerc
include mypy.ini

recursive-include tests *.py *.whl *.gz deprecated-pypirc
recursive-include docs *.bat *.empty *.py *.rst Makefile *.txt
Expand Down
9 changes: 9 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[mypy]
; TODO: check_untyped_defs = True
; TODO: Make this more granular; docs recommend it as a "last resort"
; https://mypy.readthedocs.io/en/latest/existing_code.html#start-small
ignore_missing_imports = True
; NOTE: Docs recommend `normal` or `silent` for an existing codebase
; https://mypy.readthedocs.io/en/latest/running_mypy.html#following-imports
; follow_imports = skip
show_traceback = True
10 changes: 3 additions & 7 deletions tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ def test_sign_file_with_identity(monkeypatch):


def test_run_gpg_raises_exception_if_no_gpgs(monkeypatch):
replaced_check_call = pretend.raiser(
package.FileNotFoundError('not found')
)
replaced_check_call = pretend.raiser(FileNotFoundError('not found'))
monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call)
gpg_args = ('gpg', '--detach-sign', '-a', 'pypircfile')

Expand All @@ -71,9 +69,7 @@ def test_run_gpg_raises_exception_if_no_gpgs(monkeypatch):


def test_run_gpg_raises_exception_if_not_using_gpg(monkeypatch):
replaced_check_call = pretend.raiser(
package.FileNotFoundError('not found')
)
replaced_check_call = pretend.raiser(FileNotFoundError('not found'))
monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call)
gpg_args = ('not_gpg', '--detach-sign', '-a', 'pypircfile')

Expand All @@ -87,7 +83,7 @@ def test_run_gpg_falls_back_to_gpg2(monkeypatch):

def check_call(arg_list):
if arg_list[0] == 'gpg':
raise package.FileNotFoundError('gpg not found')
raise FileNotFoundError('gpg not found')

replaced_check_call = pretend.call_recorder(check_call)
monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call)
Expand Down
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ commands =
deps =
mypy
commands =
mypy --ignore-missing-imports --follow-imports=skip twine/ tests/
mypy twine tests

[testenv:lint]
deps =
Expand All @@ -58,4 +58,4 @@ deps =
commands =
{[testenv:lint-code-style]commands}
{[testenv:lint-dist-meta]commands}
-{[testenv:lint-mypy]commands}
{[testenv:lint-mypy]commands}
4 changes: 3 additions & 1 deletion twine/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import List

import glob
import os.path

from twine import exceptions

__all__ = []
__all__: List[str] = []


def _group_wheel_files_first(files):
Expand Down
59 changes: 36 additions & 23 deletions twine/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, IO, Optional, Union, Sequence, Tuple

import collections
import hashlib
import io
Expand All @@ -25,12 +27,6 @@
from twine.wininst import WinInst
from twine import exceptions

try:
FileNotFoundError = FileNotFoundError
except NameError:
FileNotFoundError = IOError # Py2


DIST_TYPES = {
"bdist_wheel": Wheel,
"bdist_wininst": WinInst,
Expand All @@ -47,9 +43,18 @@
".zip": "sdist",
}

MetadataValue = Union[str, Sequence[str], Tuple[str, IO, str]]


class PackageFile:
def __init__(self, filename, comment, metadata, python_version, filetype):
def __init__(
self,
filename: str,
comment: Optional[str],
metadata: pkginfo.Distribution,
python_version: Optional[str],
filetype: Optional[str],
) -> None:
self.filename = filename
self.basefilename = os.path.basename(filename)
self.comment = comment
Expand All @@ -59,7 +64,7 @@ def __init__(self, filename, comment, metadata, python_version, filetype):
self.safe_name = pkg_resources.safe_name(metadata.name)
self.signed_filename = self.filename + '.asc'
self.signed_basefilename = self.basefilename + '.asc'
self.gpg_signature = None
self.gpg_signature: Optional[Tuple[str, bytes]] = None

hasher = HashManager(filename)
hasher.hash()
Expand All @@ -70,7 +75,7 @@ def __init__(self, filename, comment, metadata, python_version, filetype):
self.blake2_256_digest = hexdigest.blake2

@classmethod
def from_filename(cls, filename, comment):
def from_filename(cls, filename: str, comment: None) -> 'PackageFile':
# Extract the metadata from the package
for ext, dtype in DIST_EXTENSIONS.items():
if filename.endswith(ext):
Expand All @@ -91,6 +96,7 @@ def from_filename(cls, filename, comment):
"possible."
)

py_version: Optional[str]
if dtype == "bdist_egg":
pkgd = pkg_resources.Distribution.from_filename(filename)
py_version = pkgd.py_version
Expand All @@ -103,7 +109,7 @@ def from_filename(cls, filename, comment):

return cls(filename, comment, meta, py_version, dtype)

def metadata_dictionary(self):
def metadata_dictionary(self) -> Dict[str, MetadataValue]:
meta = self.metadata
data = {
# identify release
Expand Down Expand Up @@ -157,7 +163,11 @@ def metadata_dictionary(self):

return data

def add_gpg_signature(self, signature_filepath, signature_filename):
def add_gpg_signature(
self,
signature_filepath: str,
signature_filename: str
):
if self.gpg_signature is not None:
raise exceptions.InvalidDistribution(
'GPG Signature can only be added once'
Expand All @@ -166,9 +176,9 @@ def add_gpg_signature(self, signature_filepath, signature_filename):
with open(signature_filepath, "rb") as gpg:
self.gpg_signature = (signature_filename, gpg.read())

def sign(self, sign_with, identity):
def sign(self, sign_with: str, identity: Optional[str]):
print(f"Signing {self.basefilename}")
gpg_args = (sign_with, "--detach-sign")
gpg_args: Tuple[str, ...] = (sign_with, "--detach-sign")
if identity:
gpg_args += ("--local-user", identity)
gpg_args += ("-a", self.filename)
Expand Down Expand Up @@ -207,56 +217,59 @@ class HashManager:
This will also allow us to better test this logic.
"""

def __init__(self, filename):
def __init__(self, filename: str) -> None:
"""Initialize our manager and hasher objects."""
self.filename = filename

self._md5_hasher = None
try:
self._md5_hasher = hashlib.md5()
except ValueError:
# FIPs mode disables MD5
self._md5_hasher = None
pass

self._sha2_hasher = hashlib.sha256()

self._blake_hasher = None
if blake2b is not None:
self._blake_hasher = blake2b(digest_size=256 // 8)

def _md5_update(self, content):
def _md5_update(self, content: bytes) -> None:
if self._md5_hasher is not None:
self._md5_hasher.update(content)

def _md5_hexdigest(self):
def _md5_hexdigest(self) -> Optional[str]:
if self._md5_hasher is not None:
return self._md5_hasher.hexdigest()
return None

def _sha2_update(self, content):
def _sha2_update(self, content: bytes) -> None:
if self._sha2_hasher is not None:
self._sha2_hasher.update(content)

def _sha2_hexdigest(self):
def _sha2_hexdigest(self) -> Optional[str]:
if self._sha2_hasher is not None:
return self._sha2_hasher.hexdigest()
return None

def _blake_update(self, content):
def _blake_update(self, content: bytes) -> None:
if self._blake_hasher is not None:
self._blake_hasher.update(content)

def _blake_hexdigest(self):
def _blake_hexdigest(self) -> Optional[str]:
if self._blake_hasher is not None:
return self._blake_hasher.hexdigest()
return None

def hash(self):
def hash(self) -> None:
"""Hash the file contents."""
with open(self.filename, "rb") as fp:
for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b''):
self._md5_update(content)
self._sha2_update(content)
self._blake_update(content)

def hexdigest(self):
def hexdigest(self) -> Hexdigest:
"""Return the hexdigest for the file."""
return Hexdigest(
self._md5_hexdigest(),
Expand Down