diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2472af2..f634a94 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,11 +17,12 @@ jobs: - dependencies: ruff task: make -f Makefile fmt-travis - dependencies: > + libatomic python3-hypothesis python3-justbases python3-setuptools ruff - task: make -f Makefile lint + task: PATH=${PATH}:/github/home/.local/bin make -f Makefile lint - dependencies: > python3-hypothesis python3-justbases @@ -45,7 +46,10 @@ jobs: run: > dnf install -y make + pip ${{ matrix.dependencies }} + - name: Install pyright + run: pip install --user pyright - name: ${{ matrix.task }} run: ${{ matrix.task }} diff --git a/Makefile b/Makefile index f674e8a..0637493 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: lint lint: ruff check + pyright .PHONY: test test: diff --git a/pyproject.toml b/pyproject.toml index 10bbc1d..470ed2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ target-version = "py312" line-length = 88 [tool.ruff.lint] -select = ["PL"] +select = ["B", "F", "PL"] [tool.ruff.lint.isort] known-first-party = ["justbases"] diff --git a/src/justbytes/__init__.py b/src/justbytes/__init__.py index 7f44280..6f4c1a6 100644 --- a/src/justbytes/__init__.py +++ b/src/justbytes/__init__.py @@ -81,32 +81,33 @@ # pylint: disable=wrong-import-position # CONFIGURATION -from ._config import ( - BaseConfig, - Config, - DigitsConfig, - DisplayConfig, - StringConfig, - StripConfig, - ValueConfig, -) +from ._config import BaseConfig as BaseConfig +from ._config import Config as Config +from ._config import DigitsConfig as DigitsConfig +from ._config import DisplayConfig as DisplayConfig +from ._config import StringConfig as StringConfig +from ._config import StripConfig as StripConfig +from ._config import ValueConfig as ValueConfig # ROUNDING CONSTANTS # UNIT CONSTANTS -from ._constants import ROUNDING_METHODS, UNITS, B +from ._constants import ROUNDING_METHODS as ROUNDING_METHODS +from ._constants import UNITS as UNITS +from ._constants import B as B from ._constants import BinaryUnits as _BinaryUnits from ._constants import DecimalUnits as _DecimalUnits from ._constants import RoundingMethods as _RoundingMethods # EXCEPTIONS -from ._errors import RangeError, RangeValueError +from ._errors import RangeError as RangeError +from ._errors import RangeValueError as RangeValueError # SIZE -from ._size import Range -from ._sizes import AI +from ._size import Range as Range +from ._sizes import AI as AI # VERSION -from .version import __version__ +from .version import __version__ as __version__ KB = _DecimalUnits.KB MB = _DecimalUnits.MB diff --git a/src/justbytes/_config.py b/src/justbytes/_config.py index 0806ce0..cbf9dec 100644 --- a/src/justbytes/_config.py +++ b/src/justbytes/_config.py @@ -19,6 +19,8 @@ # pylint: disable=invalid-name +from numbers import Rational + import justbases from ._constants import PRECISE_NUMERIC_TYPES, UNITS, RoundingMethods @@ -55,9 +57,9 @@ class DisplayConfig(justbases.DisplayConfig): def __init__( self, show_approx_str=True, - base_config=BaseConfig(), - digits_config=DigitsConfig(), - strip_config=StripConfig(), + base_config=BaseConfig(), # noqa: B008 # FIXME + digits_config=DigitsConfig(), # noqa: B008 # FIXME + strip_config=StripConfig(), # noqa: B008 # FIXME ): """ Initializer. @@ -106,8 +108,8 @@ class ValueConfig: def __init__( # noqa: PLR0913,PLR0917 self, - max_places=2, - min_value=1, + max_places: int | None = 2, + min_value: int | Rational = 1, binary_units=True, exact_value=False, unit=None, @@ -196,7 +198,7 @@ class Config: STRING_CONFIG = StringConfig(ValueConfig(), DisplayConfig(), justbases.String) - STRICT = False + STRICT: bool = False @classmethod def set_display_impl(cls, impl): # pragma: no cover diff --git a/src/justbytes/_constants.py b/src/justbytes/_constants.py index 8d96b36..00cd9b9 100644 --- a/src/justbytes/_constants.py +++ b/src/justbytes/_constants.py @@ -22,7 +22,6 @@ * Size units, e.g., Ki, Mi """ -import abc from numbers import Rational import justbases @@ -57,16 +56,25 @@ def __int__(self): """ The universal unit, bytes. """ -class Units(metaclass=abc.ABCMeta): +class Units: """ Generic class for units. - """ - FACTOR = abc.abstractproperty(doc="factor for each unit") + Subclasses must define FACTOR (int) and _UNITS (list). + """ - _UNITS = abc.abstractproperty(doc="ordered list of units") + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + required_attrs = ["FACTOR", "_UNITS"] + for attr in required_attrs: + if not hasattr(cls, attr): + raise TypeError( + f"Can't instantiate abstract class {cls.__name__} " + f"without class attribute {attr}" + ) _MAX_EXPONENT = None + _UNITS = [] @classmethod def UNITS(cls): diff --git a/src/justbytes/_generators.py b/src/justbytes/_generators.py index 29090c5..ea3a636 100644 --- a/src/justbytes/_generators.py +++ b/src/justbytes/_generators.py @@ -17,6 +17,10 @@ """Special purpose generators.""" +from typing import Callable, Iterable, TypeVar + +T = TypeVar("T") + def takeuntil(pred, seq): """ @@ -31,7 +35,9 @@ def takeuntil(pred, seq): break -def next_or_last(pred, seq, default=None): +def next_or_last( + pred: Callable[[T], bool], seq: Iterable[T], default: T | None = None +) -> T | None: """ Return the first element that matches the predicate or the last element in the seq. diff --git a/src/justbytes/_size.py b/src/justbytes/_size.py index bcd76c5..dbd70d3 100644 --- a/src/justbytes/_size.py +++ b/src/justbytes/_size.py @@ -25,11 +25,20 @@ """ from fractions import Fraction +from numbers import Rational +from typing import Generator import justbases -from ._config import Config -from ._constants import PRECISE_NUMERIC_TYPES, UNIT_TYPES, B, BinaryUnits, DecimalUnits +from ._config import Config, ValueConfig +from ._constants import ( + PRECISE_NUMERIC_TYPES, + UNIT_TYPES, + B, + BinaryUnits, + DecimalUnits, + Unit, +) from ._errors import ( RangeFractionalResultError, RangeNonsensicalBinOpError, @@ -46,7 +55,9 @@ class Range: _BYTES_SYMBOL = "B" @classmethod - def _as_single_number(cls, value, config): + def _as_single_number( + cls, value: Rational, config: ValueConfig + ) -> tuple[justbases.Radix, int]: """ Returns a rational value as a single number according to the specified configuration. @@ -387,7 +398,9 @@ def convertTo(self, spec=None): return self._magnitude / factor - def componentsList(self, binary_units=True): + def componentsList( + self, binary_units: bool = True + ) -> Generator[tuple[Fraction, Unit], None, None]: """ Yield a representation of this size for every unit, decomposed into a Fraction value and a unit specifier diff --git a/tests/test_deterministic/test_size/test_operations.py b/tests/test_deterministic/test_size/test_operations.py index 4a64872..9ce938a 100644 --- a/tests/test_deterministic/test_size/test_operations.py +++ b/tests/test_deterministic/test_size/test_operations.py @@ -37,11 +37,11 @@ def test_binary_operators_range(self): # ** with self.assertRaises(RangeNonsensicalBinOpError): - size ** Range(2) + size ** Range(2) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangePowerResultError): - size**2 + size**2 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - 2 ** Range(0) + 2 ** Range(0) # pyright: ignore[reportUnusedExpression] def test_binary_operators_boolean(self): """Test binary operators with a boolean result.""" @@ -49,30 +49,30 @@ def test_binary_operators_boolean(self): # < self.assertTrue(Range(0, MiB) < Range(32)) with self.assertRaises(RangeNonsensicalBinOpError): - Range(0) < 1 + Range(0) < 1 # noqa: B015 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - 1 < Range(32, TiB) + 1 < Range(32, TiB) # noqa: B015 # pyright: ignore[reportUnusedExpression] # <= self.assertTrue(Range(0, MiB) <= Range(32)) with self.assertRaises(RangeNonsensicalBinOpError): - Range(0) <= 1 + Range(0) <= 1 # noqa: B015 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - 1 <= Range(32, TiB) + 1 <= Range(32, TiB) # noqa: B015 # pyright: ignore[reportUnusedExpression] # > self.assertTrue(Range(32, MiB) > Range(32)) with self.assertRaises(RangeNonsensicalBinOpError): - Range(32) > 1 + Range(32) > 1 # noqa: B015 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - 1 > Range(0, TiB) + 1 > Range(0, TiB) # noqa: B015 # pyright: ignore[reportUnusedExpression] # >= self.assertTrue(Range(32, MiB) >= Range(32)) with self.assertRaises(RangeNonsensicalBinOpError): - Range(32) >= 1 + Range(32) >= 1 # noqa: B015 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - 1 >= Range(0, TiB) + 1 >= Range(0, TiB) # noqa: B015 # pyright: ignore[reportUnusedExpression] # != self.assertTrue(Range(32, MiB) != Range(32, GiB)) @@ -104,9 +104,9 @@ def test_exceptions(self): """Any non-size other raises an exception.""" with self.assertRaises(RangeNonsensicalBinOpError): - 2 + Range(0) + 2 + Range(0) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(0) + 2 + Range(0) + 2 # pyright: ignore[reportUnusedExpression] class DivmodTestCase(unittest.TestCase): @@ -134,15 +134,15 @@ def test_exceptions(self): """Test that exceptions are thrown.""" with self.assertRaises(RangeNonsensicalBinOpError): - 2048 // Range(12, B) + 2048 // Range(12, B) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(12) // "str" + Range(12) // "str" # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpValueError): - Range(12) // Range(0) + Range(12) // Range(0) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpValueError): - Range(12) // 0 + Range(12) // 0 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(12) // Decimal("NaN") + Range(12) // Decimal("NaN") # pyright: ignore[reportUnusedExpression] class ModTestCase(unittest.TestCase): @@ -152,15 +152,15 @@ def test_exceptions(self): """Test that exceptions are thrown.""" with self.assertRaises(RangeNonsensicalBinOpError): - 2048 % Range(12, B) + 2048 % Range(12, B) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(12) % "str" + Range(12) % "str" # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpValueError): - Range(12) % Range(0) + Range(12) % Range(0) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpValueError): - Range(12) % 0 + Range(12) % 0 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(12) % Decimal("NaN") + Range(12) % Decimal("NaN") # pyright: ignore[reportUnusedExpression] class MultiplicationTestCase(unittest.TestCase): @@ -170,11 +170,11 @@ def test_exceptions(self): """Range others are unrepresentable.""" with self.assertRaises(RangePowerResultError): - Range(0) * Range(0) + Range(0) * Range(0) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(0) * Decimal("NaN") + Range(0) * Decimal("NaN") # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(0) * "str" + Range(0) * "str" # pyright: ignore[reportUnusedExpression] class RdivmodTestCase(unittest.TestCase): @@ -254,9 +254,9 @@ def test_exceptions(self): """Any non-size other raises an exception.""" with self.assertRaises(RangeNonsensicalBinOpError): - 2 - Range(0) + 2 - Range(0) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(0) - 2 + Range(0) - 2 # pyright: ignore[reportUnusedExpression] class TruedivTestCase(unittest.TestCase): @@ -266,12 +266,12 @@ def test_exceptions(self): """Test that exceptions are thrown.""" with self.assertRaises(RangeNonsensicalBinOpError): - 2048 / Range(12, B) + 2048 / Range(12, B) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(12) / "str" + Range(12) / "str" # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpValueError): - Range(12) / Range(0) + Range(12) / Range(0) # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpValueError): - Range(12) / 0 + Range(12) / 0 # pyright: ignore[reportUnusedExpression] with self.assertRaises(RangeNonsensicalBinOpError): - Range(12) / Decimal("NaN") + Range(12) / Decimal("NaN") # pyright: ignore[reportUnusedExpression]