Skip to content

Commit 2de7311

Browse files
committed
feat: add type hints & mypy validation
1 parent 316cf02 commit 2de7311

35 files changed

+770
-374
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
runs-on: ubuntu-latest
2727
strategy:
2828
matrix:
29-
env: [generate-integration-files, lint, format]
29+
env: [generate-integration-files, type, lint, format]
3030
steps:
3131
- uses: actions/checkout@v2
3232
- name: Setup Python

.pylintrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
[BASIC]
2+
good-names =
3+
i,
4+
j,
5+
k,
6+
ex,
7+
_,
8+
T,
9+
110
[MESSAGES CONTROL]
211
disable =
312
missing-class-docstring,

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ adheres to [Semantic Versioning](https://semver.org/).
99

1010
[unreleased]: https://github.com/rogdham/python-xz/compare/v0.2.0...HEAD
1111

12+
### :boom: Breaking changes
13+
14+
- The `filename` argument of `XZFile` is now mandatory; this change should have very
15+
limited impact as not providing it makes no sense and would have raised a `TypeError`,
16+
plus it was already mandatory on `xz.open`
17+
18+
### :rocket: Added
19+
20+
- Type hints
21+
22+
### :house: Internal
23+
24+
- Type validation with mypy
25+
- Distribute `py.typed` file in conformance with [PEP 561]
26+
27+
[pep 561]: https://www.python.org/dev/peps/pep-0561/
28+
1229
## [0.2.0] - 2021-10-23
1330

1431
[0.2.0]: https://github.com/rogdham/python-xz/releases/tag/v0.2.0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Pure Python implementation of the XZ file format with random access support
66

77
_Leveraging the lzma module for fast (de)compression_
88

9-
[![GitHub build status](https://img.shields.io/github/workflow/status/rogdham/python-xz/build/master)](https://github.com/rogdham/python-xz/actions?query=branch:master) [![Release on PyPI](https://img.shields.io/pypi/v/python-xz)](https://pypi.org/project/python-xz/) [![Code coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/rogdham/python-xz/search?q=fail+under&type=Code) [![MIT License](https://img.shields.io/pypi/l/python-xz)](https://github.com/Rogdham/python-xz/blob/master/LICENSE.txt)
9+
[![GitHub build status](https://img.shields.io/github/workflow/status/rogdham/python-xz/build/master)](https://github.com/rogdham/python-xz/actions?query=branch:master) [![Release on PyPI](https://img.shields.io/pypi/v/python-xz)](https://pypi.org/project/python-xz/) [![Code coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/rogdham/python-xz/search?q=fail+under&type=Code) [![Mypy type checker](https://img.shields.io/badge/type_checker-mypy-informational)](https://mypy.readthedocs.io/) [![MIT License](https://img.shields.io/pypi/l/python-xz)](https://github.com/Rogdham/python-xz/blob/master/LICENSE.txt)
1010

1111
---
1212

dev-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# install + dependencies
22
-e .
33

4+
# typing
5+
mypy
6+
47
# tests
58
coverage
69
pytest

mypy.ini

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[mypy]
2+
# section names refer to the documentation
3+
# https://mypy.readthedocs.io/en/stable/config_file.html
4+
5+
# Import discovery
6+
files = src
7+
ignore_missing_imports = False
8+
follow_imports = normal
9+
10+
# Platform configuration
11+
python_version = 3.9
12+
13+
# Disallow dynamic typing
14+
disallow_any_unimported = True
15+
disallow_any_decorated = True
16+
disallow_any_generics = True
17+
disallow_subclassing_any = True
18+
19+
# Untyped definitions and calls
20+
disallow_untyped_calls = True
21+
disallow_untyped_defs = True
22+
disallow_incomplete_defs = True
23+
check_untyped_defs = True
24+
disallow_untyped_decorators = True
25+
26+
# None and Optional handling
27+
no_implicit_optional = True
28+
strict_optional = True
29+
30+
# Configuring warning
31+
warn_redundant_casts = True
32+
warn_unused_ignores = True
33+
warn_no_return = True
34+
warn_return_any = True
35+
warn_unreachable = True
36+
37+
# Supressing errors
38+
show_none_errors = True
39+
ignore_errors = False
40+
41+
# Miscellaneous strictness flags
42+
strict_equality = True
43+
44+
# Configuring error messages
45+
show_error_context = True
46+
show_error_codes = True
47+
48+
# Miscellaneous
49+
warn_unused_configs = True

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ classifiers =
2323
Topic :: System :: Archiving :: Compression
2424

2525
[options]
26+
include_package_data = True
2627
package_dir = =src
2728
packages = xz
2829
setup_requires =

src/xz/block.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from io import DEFAULT_BUFFER_SIZE, SEEK_SET
22
from lzma import FORMAT_XZ, LZMACompressor, LZMADecompressor, LZMAError
3+
from typing import Tuple, Union
34

45
from xz.common import (
56
XZError,
@@ -9,12 +10,19 @@
910
parse_xz_index,
1011
)
1112
from xz.io import IOAbstract, IOCombiner, IOStatic
13+
from xz.typing import _LZMAFiltersType, _LZMAPresetType
1214

1315

1416
class BlockRead:
1517
read_size = DEFAULT_BUFFER_SIZE
1618

17-
def __init__(self, fileobj, check, unpadded_size, uncompressed_size):
19+
def __init__(
20+
self,
21+
fileobj: IOAbstract,
22+
check: int,
23+
unpadded_size: int,
24+
uncompressed_size: int,
25+
) -> None:
1826
self.length = uncompressed_size
1927
self.fileobj = IOCombiner(
2028
IOStatic(create_xz_header(check)),
@@ -25,12 +33,12 @@ def __init__(self, fileobj, check, unpadded_size, uncompressed_size):
2533
)
2634
self.reset()
2735

28-
def reset(self):
36+
def reset(self) -> None:
2937
self.fileobj.seek(0, SEEK_SET)
3038
self.pos = 0
3139
self.decompressor = LZMADecompressor(format=FORMAT_XZ)
3240

33-
def decompress(self, pos, size):
41+
def decompress(self, pos: int, size: int) -> bytes:
3442
if pos < self.pos:
3543
self.reset()
3644

@@ -63,24 +71,30 @@ def decompress(self, pos, size):
6371

6472

6573
class BlockWrite:
66-
def __init__(self, fileobj, check, preset, filters):
74+
def __init__(
75+
self,
76+
fileobj: IOAbstract,
77+
check: int,
78+
preset: _LZMAPresetType,
79+
filters: _LZMAFiltersType,
80+
) -> None:
6781
self.fileobj = fileobj
6882
self.check = check
6983
self.compressor = LZMACompressor(FORMAT_XZ, check, preset, filters)
7084
self.pos = 0
7185
if self.compressor.compress(b"") != create_xz_header(check):
7286
raise XZError("block: compressor header")
7387

74-
def _write(self, data):
88+
def _write(self, data: bytes) -> None:
7589
if data:
7690
self.fileobj.seek(self.pos)
7791
self.fileobj.write(data)
7892
self.pos += len(data)
7993

80-
def compress(self, data):
94+
def compress(self, data: bytes) -> None:
8195
self._write(self.compressor.compress(data))
8296

83-
def finish(self):
97+
def finish(self) -> Tuple[int, int]:
8498
data = self.compressor.flush()
8599

86100
# footer
@@ -102,26 +116,26 @@ def finish(self):
102116
class XZBlock(IOAbstract):
103117
def __init__(
104118
self,
105-
fileobj,
106-
check,
107-
unpadded_size,
108-
uncompressed_size,
109-
preset=None,
110-
filters=None,
119+
fileobj: IOAbstract,
120+
check: int,
121+
unpadded_size: int,
122+
uncompressed_size: int,
123+
preset: _LZMAPresetType = None,
124+
filters: _LZMAFiltersType = None,
111125
):
112126
super().__init__(uncompressed_size)
113127
self.fileobj = fileobj
114128
self.check = check
115129
self.preset = preset
116130
self.filters = filters
117131
self.unpadded_size = unpadded_size
118-
self.operation = None
132+
self.operation: Union[BlockRead, BlockWrite, None] = None
119133

120134
@property
121-
def uncompressed_size(self):
135+
def uncompressed_size(self) -> int:
122136
return self._length
123137

124-
def _read(self, size):
138+
def _read(self, size: int) -> bytes:
125139
# enforce read mode
126140
if not isinstance(self.operation, BlockRead):
127141
self._write_end()
@@ -138,10 +152,10 @@ def _read(self, size):
138152
except LZMAError as ex:
139153
raise XZError(f"block: error while decompressing: {ex}") from ex
140154

141-
def writable(self):
155+
def writable(self) -> bool:
142156
return isinstance(self.operation, BlockWrite) or not self._length
143157

144-
def _write(self, data):
158+
def _write(self, data: bytes) -> int:
145159
# enforce write mode
146160
if not isinstance(self.operation, BlockWrite):
147161
self.operation = BlockWrite(
@@ -155,14 +169,14 @@ def _write(self, data):
155169
self.operation.compress(data)
156170
return len(data)
157171

158-
def _write_after(self):
172+
def _write_after(self) -> None:
159173
if isinstance(self.operation, BlockWrite):
160174
self.unpadded_size, uncompressed_size = self.operation.finish()
161175
if uncompressed_size != self.uncompressed_size:
162176
raise XZError("block: compressor uncompressed size")
163177
self.operation = None
164178

165-
def _truncate(self, size):
179+
def _truncate(self, size: int) -> None:
166180
# thanks to the writable method, we are sure that length is zero
167181
# so we don't need to handle the case of truncating in middle of the block
168182
self.seek(size)

src/xz/common.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from binascii import crc32 as crc32int
22
import lzma
33
from struct import pack, unpack
4+
from typing import List, Tuple, cast
45

56
HEADER_MAGIC = b"\xfd7zXZ\x00"
67
FOOTER_MAGIC = b"YZ"
@@ -10,7 +11,7 @@ class XZError(Exception):
1011
pass
1112

1213

13-
def encode_mbi(value):
14+
def encode_mbi(value: int) -> bytes:
1415
data = bytearray()
1516
while value >= 0x80:
1617
data.append((value & 0x7F) | 0x80)
@@ -19,7 +20,7 @@ def encode_mbi(value):
1920
return data
2021

2122

22-
def decode_mbi(data):
23+
def decode_mbi(data: bytes) -> Tuple[int, int]:
2324
value = 0
2425
for size, byte in enumerate(data):
2526
value |= (byte & 0x7F) << (size * 7)
@@ -28,30 +29,30 @@ def decode_mbi(data):
2829
raise XZError("invalid mbi")
2930

3031

31-
def crc32(data):
32+
def crc32(data: bytes) -> bytes:
3233
return pack("<I", crc32int(data))
3334

3435

35-
def round_up(value):
36+
def round_up(value: int) -> int:
3637
remainder = value % 4
3738
if remainder:
3839
return value - remainder + 4
3940
return value
4041

4142

42-
def pad(value):
43+
def pad(value: int) -> bytes:
4344
return b"\x00" * (round_up(value) - value)
4445

4546

46-
def create_xz_header(check):
47+
def create_xz_header(check: int) -> bytes:
4748
if not 0 <= check <= 0xF:
4849
raise XZError("header check")
4950
# stream header
5051
flags = pack("<BB", 0, check)
5152
return HEADER_MAGIC + flags + crc32(flags)
5253

5354

54-
def create_xz_index_footer(check, records):
55+
def create_xz_index_footer(check: int, records: List[Tuple[int, int]]) -> bytes:
5556
if not 0 <= check <= 0xF:
5657
raise XZError("footer check")
5758
# index
@@ -70,20 +71,23 @@ def create_xz_index_footer(check, records):
7071
return index + footer
7172

7273

73-
def parse_xz_header(header):
74+
def parse_xz_header(header: bytes) -> int:
7475
if len(header) != 12:
7576
raise XZError("header length")
7677
if header[:6] != HEADER_MAGIC:
7778
raise XZError("header magic")
7879
if crc32(header[6:8]) != header[8:12]:
7980
raise XZError("header crc32")
80-
flag_first_byte, check = unpack("<BB", header[6:8])
81+
flag_first_byte, check = cast(
82+
Tuple[int, int],
83+
unpack("<BB", header[6:8]),
84+
)
8185
if flag_first_byte or not 0 <= check <= 0xF:
8286
raise XZError("header flags")
8387
return check
8488

8589

86-
def parse_xz_index(index):
90+
def parse_xz_index(index: bytes) -> List[Tuple[int, int]]:
8791
if len(index) < 8 or len(index) % 4:
8892
raise XZError("index length")
8993
index = memoryview(index)
@@ -115,14 +119,17 @@ def parse_xz_index(index):
115119
return records
116120

117121

118-
def parse_xz_footer(footer):
122+
def parse_xz_footer(footer: bytes) -> Tuple[int, int]:
119123
if len(footer) != 12:
120124
raise XZError("footer length")
121125
if footer[10:12] != FOOTER_MAGIC:
122126
raise XZError("footer magic")
123127
if crc32(footer[4:10]) != footer[:4]:
124128
raise XZError("footer crc32")
125-
backward_size, flag_first_byte, check = unpack("<IBB", footer[4:10])
129+
backward_size, flag_first_byte, check = cast(
130+
Tuple[int, int, int],
131+
unpack("<IBB", footer[4:10]),
132+
)
126133
backward_size = (backward_size + 1) * 4
127134
if flag_first_byte or not 0 <= check <= 0xF:
128135
raise XZError("footer flags")

0 commit comments

Comments
 (0)