Skip to content

Commit

Permalink
Add type annotations via MonkeyType (#469)
Browse files Browse the repository at this point in the history
* Initial version of mypy.ini

* Added _stubs folder and type hinted cli.py and __main__.py

* Move tox config to mypy.ini

* Ignore irrelevant errors

* Tidy up mypy.ini

* Remove stubs

* Apply monkeytype to twine.repository

* Clean up twine.repository annotations

* Apply monkeytype to twine.package

* Clean up twine.package annotations

* Don't ignore lint-mypy

* Clean up after rebase
  • Loading branch information
bhrutledge authored and sigmavirus24 committed Oct 25, 2019
1 parent 5a87fa8 commit f43ef0a
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 50 deletions.
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

0 comments on commit f43ef0a

Please sign in to comment.