diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d25c85..401e50c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,18 +9,20 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9"] + python: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9"] name: "Test: Python ${{ matrix.python }}" steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - run: | - python -m pip install --upgrade pip - pip install pytest - - run: pip install . - - run: pytest -v + - uses: pypa/hatch@install + - run: hatch test -v --cover --include python=$(echo ${{ matrix.python }} | tr -d '-') + - run: hatch run coverage:xml + - uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} typecheck: name: Type check runs-on: ubuntu-latest @@ -28,14 +30,16 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.12" - - run: | - python -m pip install --upgrade pip - pip install pytest mypy - - run: mypy --strict src test + python-version: "3.13" + - uses: pypa/hatch@install + - run: hatch run types:check fmt: - name: Format + name: Format and lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: psf/black@stable + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: pypa/hatch@install + - run: hatch fmt --check diff --git a/.gitignore b/.gitignore index bf45d22..62f198a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -*.egg-info *.pyc -build/ dist/ +.coverage +coverage.xml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..35cde5c --- /dev/null +++ b/codecov.yml @@ -0,0 +1,4 @@ +coverage: + status: + project: off + patch: off diff --git a/pyproject.toml b/pyproject.toml index aec0487..c34fd7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,59 @@ -[tool.black] -line-length = 100 -exclude = ''' -/( - \.git - | \.github - | .*\.egg-info - | build - | dist - )/ -''' +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "bin2coe" +authors = [ + { name = "Anish Athalye", email = "me@anishathalye.com" }, +] +description = "A tool to convert binary files to COE files" +readme = "README.md" +requires-python = ">=3.6" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Topic :: Utilities", +] +keywords = ["xilinx", "coe", "bram"] +dynamic = ["version"] + +[project.scripts] +bin2coe = "bin2coe.cli:main" + +[project.urls] +homepage = "https://github.com/anishathalye/bin2coe" +repository = "https://github.com/anishathalye/bin2coe.git" +issues = "https://github.com/anishathalye/bin2coe/issues" + +[tool.hatch.version] +path = "src/bin2coe/__init__.py" + +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.9"] + +[tool.hatch.envs.types] +extra-dependencies = [ + "pytest", + "mypy>=1.0.0", +] + +[tool.hatch.envs.types.scripts] +check = "mypy --strict --install-types --non-interactive {args:src tests}" + +[tool.hatch.envs.coverage] +detached = true +dependencies = [ + "coverage", +] + +[tool.hatch.envs.coverage.scripts] +html = "coverage html" +xml = "coverage xml" + +[tool.ruff.lint] +ignore = [ + "FA100", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index 2debebd..0000000 --- a/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -from setuptools import setup, find_packages -from os import path -import re - - -here = path.dirname(__file__) - - -with open(path.join(here, "README.md"), encoding="utf-8") as f: - long_description = f.read() - - -def read(*names, **kwargs): - with open(path.join(here, *names), encoding=kwargs.get("encoding", "utf8")) as fp: - return fp.read() - - -def find_version(*file_paths): - version_file = read(*file_paths) - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - - -setup( - name="bin2coe", - python_requires=">=3.4", - version=find_version("src", "bin2coe", "__init__.py"), - description="A tool to convert binary files to COE files", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/anishathalye/bin2coe", - author="Anish Athalye", - author_email="me@anishathalye.com", - license="MIT", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Topic :: Utilities", - ], - keywords="xilinx coe bram", - packages=find_packages("src"), - package_dir={"": "src"}, - entry_points={ - "console_scripts": [ - "bin2coe=bin2coe.cli:main", - ], - }, -) diff --git a/src/bin2coe/cli.py b/src/bin2coe/cli.py index a689cf0..ab9e36c 100644 --- a/src/bin2coe/cli.py +++ b/src/bin2coe/cli.py @@ -1,9 +1,8 @@ -from .convert import convert -from argparse import ArgumentParser import sys +from argparse import ArgumentParser +from typing import NoReturn - -from typing import NoReturn, BinaryIO +from bin2coe.convert import convert def main() -> None: @@ -19,15 +18,15 @@ def main() -> None: options = parser.parse_args() # check radix - if not (2 <= options.radix <= 36): + if not (2 <= options.radix <= 36): # noqa: PLR2004 error("unsupported radix, must be between 2 and 36") - if options.mem and not options.radix in [2, 16]: + if options.mem and options.radix not in [2, 16]: error("mem requires radix 2 or 16") # check width - if not 8 <= options.width: + if not options.width >= 8: # noqa: PLR2004 error("width must be >= 8") - if not options.width & (options.width - 1) == 0: + if options.width & options.width - 1 != 0: error("width must be a power of 2") # if fill is specified, then depth must be specified too; otherwise, depth @@ -41,7 +40,7 @@ def main() -> None: try: fill = int(options.fill, options.radix) except ValueError: - error("invalid fill ({}) for radix ({})".format(options.fill, options.radix)) + error(f"invalid fill ({options.fill}) for radix ({options.radix})") with open(options.input, "rb") as f: data = f.read() @@ -54,26 +53,20 @@ def main() -> None: if bits % options.width != 0: extra = options.width - bits % options.width if extra % 8 != 0: - error("cannot infer depth, {} total bits, width {}".format(bits, options.width)) + error(f"cannot infer depth, {bits} total bits, width {options.width}") extra_words = extra // 8 data = data + bytes(extra_words) bits = 8 * len(data) depth = bits // options.width + elif fill is None: + if bits != options.width * depth: + error(f"memory size / file size mismatch: {depth} x {options.width} bit != {bits}") else: - # validate depth - if fill is None: - if bits != options.width * depth: - error( - "memory size / file size mismatch: {} x {} bit != {}".format( - depth, options.width, bits - ) - ) - else: - # make sure it fills an integer number of words - if bits % options.width != 0: - error("data must fill an integer number of words") - if bits > options.width * depth: - error("memory size too small: {} x {} bit < {}".format(depth, options.width, bits)) + # make sure it fills an integer number of words + if bits % options.width != 0: + error("data must fill an integer number of words") + if bits > options.width * depth: + error(f"memory size too small: {depth} x {options.width} bit < {bits}") with open(options.output, "wb") as f: convert( @@ -89,5 +82,5 @@ def main() -> None: def error(msg: str) -> NoReturn: - print("error: {}".format(msg), file=sys.stderr) - exit(1) + print(f"error: {msg}", file=sys.stderr) # noqa: T201 + sys.exit(1) diff --git a/src/bin2coe/convert.py b/src/bin2coe/convert.py index 671ee1a..4a64c90 100644 --- a/src/bin2coe/convert.py +++ b/src/bin2coe/convert.py @@ -1,4 +1,4 @@ -from typing import Iterable, TypeVar, BinaryIO, List, Iterator, Optional +from typing import BinaryIO, Iterable, Iterator, List, Optional, TypeVar T = TypeVar("T") @@ -14,7 +14,7 @@ def chunks(it: Iterable[T], n: int) -> Iterator[List[T]]: yield res -def word_to_int(word: List[int], little_endian: bool) -> int: +def word_to_int(word: List[int], *, little_endian: bool) -> int: if not little_endian: word = list(reversed(word)) value = 0 @@ -26,7 +26,8 @@ def word_to_int(word: List[int], little_endian: bool) -> int: def format_int(num: int, base: int, pad_width: int = 0) -> str: chars = "0123456789abcdefghijklmnopqrstuvwxyz" if num < 0: - raise ValueError("negative numbers not supported") + msg = "negative numbers not supported" + raise ValueError(msg) res = [] res.append(chars[num % base]) while num >= base: @@ -44,12 +45,13 @@ def convert( depth: int, fill: Optional[int], radix: int, + *, little_endian: bool = True, mem: bool = False, ) -> None: pad_width = len(format_int(2**width - 1, radix)) if not mem: - output.write("memory_initialization_radix = {};\n".format(radix).encode("utf8")) + output.write(f"memory_initialization_radix = {radix};\n".encode()) output.write(b"memory_initialization_vector =\n") rows = 0 for word in chunks(data, width // 8): @@ -57,10 +59,12 @@ def convert( if not mem: output.write(b",") output.write(b"\n") - output.write(format_int(word_to_int(word, little_endian), radix, pad_width).encode("utf8")) + output.write(format_int(word_to_int(word, little_endian=little_endian), radix, pad_width).encode("utf8")) rows += 1 if rows < depth: - assert fill is not None + if fill is None: + msg = "fill must not be 'None' if memory is not filled by values" + raise ValueError(msg) while rows < depth: if not mem: output.write(b",") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/bin2coe/test_convert.py b/tests/test_convert.py similarity index 97% rename from test/bin2coe/test_convert.py rename to tests/test_convert.py index d44e201..0e27483 100644 --- a/test/bin2coe/test_convert.py +++ b/tests/test_convert.py @@ -1,7 +1,6 @@ -from bin2coe.convert import * import io -import pytest +from bin2coe.convert import chunks, convert def test_chunks() -> None: