Skip to content

Commit 386b60e

Browse files
authored
chore: Add mypy as a type checker for repository (#11)
- Type hint modernization using mypy as a type checker on pre-commit -- solving all typing issues in the library. - Bug fix when doing a proxy assignment. - Remove Python 3.9 from labels. - Add pyupgrade and isort to the default ruff rules (for typing upgrades with Python 3.10).
1 parent f7a4bfa commit 386b60e

10 files changed

Lines changed: 122 additions & 104 deletions

File tree

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ repos:
77
- '--fix'
88
- '--exit-non-zero-on-fix'
99
- id: ruff-format
10+
- repo: 'https://github.com/pre-commit/mirrors-mypy'
11+
rev: v1.20.0
12+
hooks:
13+
- id: mypy
14+
additional_dependencies:
15+
- aiohttp
16+
- async-lru
17+
- rich
1018
- repo: 'https://github.com/pre-commit/pre-commit-hooks'
1119
rev: v5.0.0
1220
hooks:

crpy/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from crpy.registry import RegistryInfo
1+
from crpy.common import BaseCrpyError, HTTPConnectionError, UnauthorizedError
22
from crpy.image import Blob, Image
3-
from crpy.common import HTTPConnectionError, UnauthorizedError, BaseCrpyError
3+
from crpy.registry import RegistryInfo
44
from crpy.version import __version__
55

66
__all__ = [

crpy/auth.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
async def get_token(
77
url: str,
8-
username: str = None,
9-
password: str = None,
10-
b64_token: str = None,
11-
aiohttp_kwargs: dict = None,
8+
username: str | None = None,
9+
password: str | None = None,
10+
b64_token: str | None = None,
11+
aiohttp_kwargs: dict | None = None,
1212
):
1313
# get the credentials here.
1414
# I'll use the simple auth since it mostly works
1515
headers = {}
1616
if username and password:
17-
token = b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
17+
token = b64encode(f"{username}:{password}".encode()).decode("ascii")
1818
headers = {"Authorization": f"Basic {token}"}
1919
elif b64_token:
2020
headers = {"Authorization": f"Basic {b64_token}"}

crpy/cmd.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from rich.table import Table
99
from rich.text import Text
1010

11-
1211
from crpy.common import HTTPConnectionError, UnauthorizedError
1312
from crpy.registry import RegistryInfo
1413
from crpy.storage import (

crpy/common.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import io
55
import json
66
import socket
7-
from dataclasses import dataclass
8-
from typing import List, Optional, Union
7+
from dataclasses import dataclass, field
98

109
import aiohttp
1110

@@ -14,19 +13,19 @@
1413
class Response:
1514
status: int
1615
data: bytes
17-
headers: Optional[dict] = None
16+
headers: dict = field(default_factory=dict)
1817

1918
def json(self) -> dict:
2019
return json.loads(self.data)
2120

2221

2322
async def _request(
2423
url,
25-
headers: dict = None,
26-
params: dict = None,
27-
data: Union[dict, bytes] = None,
24+
headers: dict | None = None,
25+
params: dict | None = None,
26+
data: dict | bytes | None = None,
2827
method: str = "post",
29-
aiohttp_kwargs: dict = None,
28+
aiohttp_kwargs: dict | None = None,
3029
) -> Response:
3130
aiohttp_kwargs = aiohttp_kwargs or {}
3231
try:
@@ -38,15 +37,15 @@ async def _request(
3837
raise HTTPConnectionError(str(e))
3938

4039

41-
async def _stream(url, headers: dict = None, aiohttp_kwargs: dict = None):
40+
async def _stream(url, headers: dict | None = None, aiohttp_kwargs: dict | None = None):
4241
aiohttp_kwargs = aiohttp_kwargs or {}
4342
async with aiohttp.ClientSession(trust_env=True) as session:
4443
async with session.get(url, headers=headers, **aiohttp_kwargs) as response:
4544
async for data, _ in response.content.iter_chunks():
4645
yield data
4746

4847

49-
def compute_sha256(file: Union[str, io.BytesIO, bytes], use_prefix: bool = True):
48+
def compute_sha256(file: str | io.BytesIO | bytes, use_prefix: bool = True):
5049
# If input is a string, consider it a filename
5150
if isinstance(file, str):
5251
with open(file, "rb") as f:
@@ -92,7 +91,7 @@ def architecture(self) -> str:
9291
return self.value.split("/")[1]
9392

9493
@property
95-
def variant(self) -> Optional[str]:
94+
def variant(self) -> str | None:
9695
split_value = self.value.split("/")
9796
return split_value[2] if len(split_value) > 2 else None
9897

@@ -104,7 +103,7 @@ def platform_from_dict(platform: dict) -> str:
104103
return base_str
105104

106105

107-
async def resolve_hostname(hostname: str, family: int = socket.AF_UNSPEC) -> List[str]:
106+
async def resolve_hostname(hostname: str, family: int = socket.AF_UNSPEC) -> list[str]:
108107
"""
109108
Resolves a hostname to a sorted list of unique IP addresses.
110109

crpy/image.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@
44
import pathlib
55
import tarfile
66
import tempfile
7+
from collections.abc import Sequence
78
from dataclasses import dataclass
8-
from typing import List, Optional, Union
99

1010
from crpy.common import compute_sha256
1111

12-
INPUT_TYPES = Union[bytes, pathlib.Path, str, io.StringIO, dict, None]
13-
1412

1513
@dataclass
1614
class Blob:
17-
path: Optional[pathlib.Path] = None
18-
content: Optional[bytes] = None
19-
filename: Optional[pathlib.Path] = None
20-
digest: Optional[str] = None
15+
path: pathlib.Path | None = None
16+
content: bytes | None = None
17+
filename: pathlib.Path | None = None
18+
digest: str | None = None
2119

2220
@classmethod
23-
def from_any(cls, value: INPUT_TYPES, digest: str = None) -> "Blob":
24-
if isinstance(value, str):
21+
def from_any(cls, value: "INPUT_TYPES", digest: str | None = None) -> "Blob":
22+
if isinstance(value, Blob):
23+
return value
24+
elif isinstance(value, str):
2525
return cls(pathlib.Path(value), digest=digest)
2626
elif isinstance(value, pathlib.Path):
2727
return cls(value, digest=digest)
@@ -31,12 +31,14 @@ def from_any(cls, value: INPUT_TYPES, digest: str = None) -> "Blob":
3131
return cls(None, value.read().encode(), digest=digest)
3232
elif isinstance(value, dict):
3333
return cls(None, json.dumps(value).encode(), digest=digest)
34+
raise ValueError(f"Unsupported type {type(value)}")
3435

35-
def as_bytes(self):
36+
def as_bytes(self) -> bytes:
3637
if self.path:
3738
return self.path.read_bytes()
38-
else:
39-
return self.content
39+
if self.content is None:
40+
raise ValueError("Blob has no path or content")
41+
return self.content
4042

4143
def as_dict(self):
4244
return json.loads(self.as_bytes())
@@ -48,19 +50,22 @@ def sha256_sum(self):
4850
return self.digest
4951

5052

53+
INPUT_TYPES = bytes | pathlib.Path | str | io.StringIO | dict | Blob | None
54+
55+
5156
class Image:
5257
"""
5358
Component to interact with docker images for the purpose of building and generating tar-files with the correct
5459
layers. This can be populated at will and written to disk, having the individual blobs modified.
5560
"""
5661

57-
def __init__(self, config: dict = None, manifest: dict = None, layers: List[bytes] = None):
58-
self._config: Optional[Blob] = None
59-
self._manifest: Optional[Blob] = None
60-
self._layers: Optional[List[Blob]] = None
62+
def __init__(self, config: INPUT_TYPES, manifest: INPUT_TYPES, layers: Sequence[INPUT_TYPES]):
63+
self._config: Blob
64+
self._manifest: Blob
65+
self._layers: list[Blob]
6166
self.config = config
6267
self.manifest = manifest
63-
self.layers = layers or []
68+
self.layers = layers
6469

6570
@property
6671
def manifest(self) -> Blob:
@@ -79,14 +84,14 @@ def config(self, value: INPUT_TYPES):
7984
self._config = Blob.from_any(value)
8085

8186
@property
82-
def layers(self) -> List[Blob]:
87+
def layers(self) -> list[Blob]:
8388
return self._layers
8489

8590
@layers.setter
86-
def layers(self, layers: List[INPUT_TYPES]):
91+
def layers(self, layers: Sequence[INPUT_TYPES]):
8792
self._layers = [Blob.from_any(layer) for layer in layers]
8893

89-
def to_disk(self, filename: pathlib.Path, tags: List[str] = None):
94+
def to_disk(self, filename: pathlib.Path | io.BytesIO | str, tags: list[str] | None = None):
9095
with tempfile.TemporaryDirectory() as temp_dir:
9196
web_manifest = self.manifest.as_dict()
9297
config_filename = f"{web_manifest['config']['digest'].split(':')[1]}.json"
@@ -112,10 +117,10 @@ def to_disk(self, filename: pathlib.Path, tags: List[str] = None):
112117
json.dump(manifest, outfile)
113118

114119
if isinstance(filename, io.BytesIO):
115-
output_kwargs = {"fileobj": filename, "mode": "w"}
120+
tar_open = tarfile.open(fileobj=filename, mode="w")
116121
else:
117-
output_kwargs = {"name": filename, "mode": "w"}
118-
with tarfile.open(**output_kwargs) as tar_out:
122+
tar_open = tarfile.open(name=filename, mode="w")
123+
with tar_open as tar_out:
119124
os.chdir(temp_dir)
120125
tar_out.add(".")
121126
os.chdir("..")

0 commit comments

Comments
 (0)