From eb854fe849f2e0b1eb81ef05157b680ab09bdd50 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 21 Oct 2024 20:44:07 -0700 Subject: [PATCH] Turn on strict static analysis checking --- dev/bump.py | 51 ++++++------ dev/clean.py | 3 + dev/formatting.py | 23 +++--- dev/patch-doctest.py | 17 ++-- dev/test-runner.py | 24 +++--- mypy_stubs/setuptools_scm.pyi | 8 ++ profiling/profile.py | 100 +++++++++++++----------- pyproject.toml | 93 ++++++++++++---------- setup.py | 10 ++- src/fastnumbers/__init__.py | 14 ++-- tests/builtin_grammar.py | 2 + tests/builtin_support.py | 14 ++-- tests/conftest.py | 2 + tests/test_array.py | 19 +++-- tests/test_builtin_float.py | 27 ++++--- tests/test_builtin_int.py | 44 ++++++----- tests/test_fastnumbers.py | 121 ++++++++++++++--------------- tests/test_fastnumbers_examples.py | 10 ++- tox.ini | 3 +- 19 files changed, 328 insertions(+), 257 deletions(-) mode change 100644 => 100755 dev/bump.py mode change 100644 => 100755 dev/formatting.py mode change 100644 => 100755 dev/patch-doctest.py mode change 100644 => 100755 dev/test-runner.py create mode 100644 mypy_stubs/setuptools_scm.pyi mode change 100644 => 100755 profiling/profile.py diff --git a/dev/bump.py b/dev/bump.py old mode 100644 new mode 100755 index 1f45752d..b8eb9fe0 --- a/dev/bump.py +++ b/dev/bump.py @@ -1,9 +1,16 @@ +#! /usr/bin/env python + """ +Bump version of fastnumbers. + Cross-platform bump of version with special CHANGELOG modification. INTENDED TO BE CALLED FROM PROJECT ROOT, NOT FROM dev/! """ +from __future__ import annotations + import datetime +import pathlib import subprocess import sys @@ -30,8 +37,8 @@ sys.exit('bump_type must be one of "major", "minor", or "patch"!') -def git(cmd, *args): - """Wrapper for calling git""" +def git(cmd: str, *args: str) -> None: + """Call git.""" try: subprocess.run(["git", cmd, *args], check=True, text=True) except subprocess.CalledProcessError as e: @@ -58,26 +65,26 @@ def git(cmd, *args): next_version = ".".join(map(str, version_components)) # Update the changelog. -with open("CHANGELOG.md") as fl: - changelog = fl.read() - - # Add a date to this entry. - changelog = changelog.replace( - "Unreleased", - f"Unreleased\n---\n\n[{next_version}] - {datetime.datetime.now():%Y-%m-%d}", - ) - - # Add links to the entries. - changelog = changelog.replace( - "", - "\n[{new}]: {url}/{current}...{new}".format( - new=next_version, - current=current_version, - url="https://github.com/SethMMorton/fastnumbers/compare", - ), - ) -with open("CHANGELOG.md", "w") as fl: - fl.write(changelog) +changelog = pathlib.Path("CHANGELOG.md").read_text() + +# Add a date to this entry. +changelog = changelog.replace( + "Unreleased", + f"Unreleased\n---\n\n[{next_version}] - {datetime.datetime.now():%Y-%m-%d}", +) + +# Add links to the entries. +changelog = changelog.replace( + "", + "\n[{new}]: {url}/{current}...{new}".format( + new=next_version, + current=current_version, + url="https://github.com/SethMMorton/fastnumbers/compare", + ), +) + +# Write the changelog +pathlib.Path("CHANGELOG.md").write_text(changelog) # Add the CHANGELOG.md changes and commit & tag. git("add", "CHANGELOG.md") diff --git a/dev/clean.py b/dev/clean.py index 090dcab6..0b9f2ee4 100755 --- a/dev/clean.py +++ b/dev/clean.py @@ -2,9 +2,12 @@ """ Cross-platform clean of working directory. + INTENDED TO BE CALLED FROM PROJECT ROOT, NOT FROM dev/! """ +from __future__ import annotations + import pathlib import shutil diff --git a/dev/formatting.py b/dev/formatting.py old mode 100644 new mode 100755 index 57eb7a6f..0f77076b --- a/dev/formatting.py +++ b/dev/formatting.py @@ -1,10 +1,13 @@ +#! /usr/bin/env python3 """ Cross-platform checking if code is appropriately formatted. + INTENDED TO BE CALLED FROM PROJECT ROOT, NOT FROM dev/! """ -import glob -import shlex +from __future__ import annotations + +import pathlib import subprocess import sys @@ -14,28 +17,26 @@ ruff = ["ruff", "format"] if check: ruff.extend(["--check", "--diff"]) -print(*map(shlex.quote, ruff)) -ruff_ret = subprocess.run(ruff) +ruff_ret = subprocess.run(ruff, check=False, text=True) # Check that C++ code is formatted -clang_format = [ +clang_format: list[pathlib.Path | str] = [ "clang-format", "--style=file:dev/clang-format.cfg", "--dry-run" if check else "-i", ] -cpp = glob.glob("src/cpp/*.cpp") -hpp = glob.glob("include/fastnumbers/*.hpp") -hpp += glob.glob("include/fastnumbers/parser/*.hpp") +cpp = list(pathlib.Path("src/cpp").glob("*.cpp")) +hpp = list(pathlib.Path("include/fastnumbers").glob("*.hpp")) +hpp.extend(pathlib.Path("include/fastnumbers/parser").glob("*.hpp")) -clang_format = clang_format + cpp + hpp -print(*map(shlex.quote, clang_format)) +clang_format += cpp + hpp clang_format_ret = subprocess.run( clang_format, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + check=False, ) -print(clang_format_ret.stdout) # Stop here if not checking if not check: diff --git a/dev/patch-doctest.py b/dev/patch-doctest.py old mode 100644 new mode 100755 index 44f41a1c..3d31f46e --- a/dev/patch-doctest.py +++ b/dev/patch-doctest.py @@ -1,6 +1,9 @@ +#! /usr/bin/env python3 """ +Patch the doctest module. + Copies doctest.py from the stdlib to the current directory, -and modifies it so that +and modifies it so that. a) It will load "*.so" files as modules just like a "*.py" file b) It recognizes functions defined in "*.so" files @@ -9,15 +12,20 @@ With these enhancements, doctest can be run on a C python extension module. """ +from __future__ import annotations + import doctest import inspect +import pathlib +import sys # Get the source file location dt_location = inspect.getsourcefile(doctest) +if dt_location is None: + sys.exit("Could not locate doctest module location.") # Read the module into memory -with open(dt_location) as fl: - doctest_str = fl.read() +doctest_str = pathlib.Path(dt_location).read_text() # Let's add the glob module. # Also define a function to detect if we could import this module name. @@ -64,5 +72,4 @@ doctest_str = "# type: ignore\n" + doctest_str # Open up the new output file and write the modified input to it. -with open("doctest.py", "w") as fl: - print(doctest_str, file=fl, end="") +pathlib.Path("doctest.py").write_text(doctest_str) diff --git a/dev/test-runner.py b/dev/test-runner.py old mode 100644 new mode 100755 index 199c17cc..535925b0 --- a/dev/test-runner.py +++ b/dev/test-runner.py @@ -1,9 +1,12 @@ +#! /usr/bin/env python3 """ -This script will run tests using gdb as a means to catch segfaults. +Run tests using gdb as a means to catch segfaults. If gdb is not installed, it just runs the tests. """ +from __future__ import annotations + import os import sys @@ -12,25 +15,22 @@ try: # Don't use gdb unless requesting debugging mode if "FN_DEBUG" not in os.environ: - raise OSError + raise OSError # noqa: TRY301 # Attempt to run pytest with debugger + # fmt: off os.execlp( "gdb", "gdb", - "-ex", - "run", - "-ex", - "bt", - "-ex", - "quit", - "--args", - my_python, - "-m", - "pytest", + "-ex", "run", + "-ex", "bt", + "-ex", "quit", + "--args", my_python, + "-m", "pytest", "--doctest-glob=README.rst", *other_args, ) + # fmt: on except OSError: # No debugger installed, just run pytest directly os.execl( diff --git a/mypy_stubs/setuptools_scm.pyi b/mypy_stubs/setuptools_scm.pyi new file mode 100644 index 00000000..a808558c --- /dev/null +++ b/mypy_stubs/setuptools_scm.pyi @@ -0,0 +1,8 @@ +import os + +def get_version( + root: os.PathLike[str] | str = ".", + version_scheme: str = ..., + local_scheme: str = ..., + relative_to: os.PathLike[str] | str | None = ..., +) -> str: ... diff --git a/profiling/profile.py b/profiling/profile.py old mode 100644 new mode 100755 index 082a74c7..7bc27a96 --- a/profiling/profile.py +++ b/profiling/profile.py @@ -1,3 +1,8 @@ +#! /usr/bin/env python +"""Profile the performance of fastnumbers.""" + +from __future__ import annotations + import copy import decimal import gc @@ -19,23 +24,30 @@ else: class Tee: + """Redirect an output stream to both file and the stream.""" + def __init__(self, stream, filepath): + """Initialize.""" self.stream = stream - self.fo = open(filepath, "w") # noqa: SIM115 + self.fo = open(filepath, "w") # noqa: SIM115, PTH123 def write(self, data): + """Write the data to the streams.""" self.stream.write(data) self.stream.flush() self.fo.write(data) def flush(self): + """Flush the buffer.""" self.stream.flush() self.fo.flush() def close(self): + """Close the open file object.""" self.fo.close() def __del__(self): + """Close the data on instance deletion.""" self.close() sys.stdout = Tee(sys.stdout, outloc) @@ -59,17 +71,17 @@ class Timer: ) def __init__(self, title): + """Initialize.""" print("### " + title) print() self.functions = [] - def add_function(self, func, label, setup="pass", iterable=False): + def add_function(self, func, label, *, setup="pass", iterable=False): """Add a function to be timed and compared.""" self.functions.append((func, setup, label, iterable)) - def time_functions(self, repeat=5): + def time_functions(self, *, repeat=5): """Time all the given functions against all input then display results.""" - # Collect the function labels to make the header of this table. # Show that the units are seconds for each. function_labels = [label + " (ms)" for _, _, label, _ in self.functions] @@ -85,7 +97,7 @@ def time_functions(self, repeat=5): row = [] for func, setup, _, iterable in self.functions: if iterable: - setup += f"; iterable = [{value!r}] * 50" + setup += f"; iterable = [{value!r}] * 50" # noqa: PLW2901 call = f"{func}(iterable)" else: call = f"{func}({value!r})" @@ -113,20 +125,23 @@ def time_functions(self, repeat=5): @staticmethod def mean(x): + """Compute the mean of the measured times.""" return math.fsum(x) / len(x) @staticmethod def stddev(x): + """Compute the standard deviation of the measured times.""" mean = Timer.mean(x) sum_of_squares = math.fsum((v - mean) ** 2 for v in x) return math.sqrt(sum_of_squares / (len(x) - 1)) @staticmethod def bold(x): + """Make text bold.""" return f"**{x}**" def _timeit(self, call, setup, repeat=5): - """Perform the actual timing and return a formatted string of the runtime""" + """Perform the actual timing and return a formatted string of the runtime.""" result = timeit.repeat(call, setup, number=100000, repeat=repeat) return self.mean(result), self.stddev(result) @@ -135,12 +150,15 @@ class Table(list): """List of strings that can be made into a Markdown table.""" def add_row(self, *elements): + """Insert a row into the table.""" self.append(list(elements)) def add_header(self, *elements): + """Insert a row into the table.""" self.add_row(*elements) def __str__(self): + """Convert the table into a markdown string.""" header = copy.deepcopy(self[0]) rows = copy.deepcopy(self[1:]) @@ -177,17 +195,15 @@ def __str__(self): def int_re(x, int_match=re.compile(r"[-+]?\d+$").match): - """Function to simulate fast_int but with regular expressions.""" + """Simulate fast_int but with regular expressions.""" try: - if int_match(x): - return int(x) - return x + return int(x) if int_match(x) else x except TypeError: return int(x) def int_try(x): - """Function to simulate fast_int but with try/except.""" + """Simulate fast_int but with try/except.""" try: return int(x) except ValueError: @@ -195,17 +211,15 @@ def int_try(x): def float_re(x, float_match=re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$").match): - """Function to simulate fast_float but with regular expressions.""" + """Simulate fast_float but with regular expressions.""" try: - if float_match(x): - return float(x) - return x + return float(x) if float_match(x) else x except TypeError: return float(x) def float_try(x): - """Function to simulate fast_float but with try/except.""" + """Simulate fast_float but with try/except.""" try: return float(x) except ValueError: @@ -217,13 +231,9 @@ def real_re( int_match=re.compile(r"[-+]?\d+$").match, real_match=re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$").match, ): - """Function to simulate fast_real but with regular expressions.""" + """Simulate fast_real but with regular expressions.""" try: - if int_match(x): - return int(x) - if real_match(x): - return float(x) - return x + return int(x) if int_match(x) else float(x) if real_match(x) else x except TypeError: if type(x) in (float, int): return x @@ -231,7 +241,7 @@ def real_re( def real_try(x): - """Function to simulate fast_real but with try/except.""" + """Simulate fast_real but with try/except.""" try: a = float(x) except ValueError: @@ -246,19 +256,15 @@ def forceint_re( int_match=re.compile(r"[-+]\d+$").match, float_match=re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$").match, ): - """Function to simulate fast_forceint but with regular expressions.""" + """Simulate fast_forceint but with regular expressions.""" try: - if int_match(x): - return int(x) - if float_match(x): - return int(float(x)) - return x + return int(x) if int_match(x) else int(float(x)) if float_match(x) else x except TypeError: return int(x) def forceint_try(x): - """Function to simulate fast_forceint but with try/except.""" + """Simulate fast_forceint but with try/except.""" try: return int(x) except ValueError: @@ -273,7 +279,7 @@ def forceint_try(x): def forceint_denoise( x, _decimal=decimal.Decimal, ceil=math.ceil, log10=math.log10, ulp=math.ulp ): - """Function to noiselessly convert a float to an integer.""" + """Noiselessly convert a float to an integer.""" try: # Integer method int_val = int(x) @@ -289,18 +295,18 @@ def forceint_denoise( raise TypeError from e def forceint_denoise_fn(x, try_forceint=fastnumbers.try_forceint): - """Function to noiselessly convert a float to an integer using fastnumbers.""" + """Noiselessly convert a float to an integer using fastnumbers.""" return try_forceint(x, denoise=True) def check_int_re(x, int_match=re.compile(r"[-+]?\d+$").match): - """Function to simulate check_int but with regular expressions.""" + """Simulate check_int but with regular expressions.""" t = type(x) return t is int if t in (float, int) else bool(int_match(x)) def check_int_try(x): - """Function to simulate check_int but with try/except.""" + """Simulate check_int but with try/except.""" try: int(x) except ValueError: @@ -312,13 +318,13 @@ def check_int_try(x): def check_float_re( x, float_match=re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$").match ): - """Function to simulate check_float but with regular expressions.""" + """Simulate check_float but with regular expressions.""" t = type(x) return t is float if t in (float, int) else bool(float_match(x)) def check_float_try(x): - """Function to simulate check_float but with try/except.""" + """Simulate check_float but with try/except.""" try: float(x) except ValueError: @@ -328,12 +334,12 @@ def check_float_try(x): def check_real_re(x, real_match=re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$").match): - """Function to simulate check_real but with regular expressions.""" + """Simulate check_real but with regular expressions.""" return type(x) in (float, int) or bool(real_match(x)) def check_real_try(x): - """Function to simulate check_real but with try/except.""" + """Simulate check_real but with try/except.""" try: float(x) except ValueError: @@ -347,19 +353,17 @@ def check_intlike_re( int_match=re.compile(r"[-+]?\d+$").match, float_match=re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$").match, ): - """Function to simulate check_intlike but with regular expressions.""" + """Simulate check_intlike but with regular expressions.""" try: - if int_match(x): - return True - if float_match(x): - return float(x).is_integer() - return False + return bool(int_match(x)) or ( + float(x).is_integer() if float_match(x) else False + ) except TypeError: return int(x) == x def check_intlike_try(x): - """Function to simulate check_intlike but with try/except.""" + """Simulate check_intlike but with try/except.""" try: a = int(x) except ValueError: @@ -374,22 +378,27 @@ def check_intlike_try(x): def fn_listcomp(iterable, func=fastnumbers.try_float): + """Execute the function in a list comprehension.""" return [func(x) for x in iterable] def fn_map(iterable, func=fastnumbers.try_float): + """Execute the function with map.""" return list(map(func, iterable)) def fn_map_option(iterable, func=fastnumbers.try_float): + """Execute the function with the map option.""" return func(iterable, map=list) def fn_map_iter_option(iterable, func=fastnumbers.try_float): + """Execute the function with the iter option.""" return list(func(iterable, map=True)) def fn_then_array(iterable, func=fastnumbers.try_float): + """Execute the function as an array.""" return np.array(func(iterable, map=list), dtype=np.float64) @@ -397,6 +406,7 @@ def fn_then_array(iterable, func=fastnumbers.try_float): def fn_into_array(iterable, func=fastnumbers.try_array, out=output): + """Execute the function into an existing array.""" func(iterable, out) diff --git a/pyproject.toml b/pyproject.toml index f23fb4e0..3c7bd2ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ Changelog = "https://github.com/SethMMorton/fastnumbers/blob/main/CHANGELOG.md" version_file = "src/fastnumbers/_version.py" [tool.mypy] +mypy_path = "mypy_stubs" plugins = "numpy.typing.mypy_plugin" [tool.pytest.ini_options] @@ -64,38 +65,7 @@ docstring-code-format = true [tool.ruff.lint] fixable = ["ALL"] -select = [ - "F", - "E", - "W", - "C90", # mccabe - "B", # flake8-bugbear - "N", # pep8-naming - "I", # isort - "UP", # pyupgrade - "YTT", # flake8-2020 - "BLE", # flake8-blind-except - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "EM", # flake8-errmsg - "EXE", # flake8-executable - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "PIE", # flake8-pie - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "Q", # flake8-quotes - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "ARG", # flake8-unused-arguments - "ERA", # eradicate - "RUF", # ruff-specific-rules -] +select = ["ALL"] ignore = [ "RUF001", # ambiguous-unicode-character-string "RUF012", # mutable class variables need annotation @@ -103,21 +73,66 @@ ignore = [ "ISC001", # single line implicit string concatenation "PT011", # pytest.raises without match "PYI001", # prefix private types with '_' + "DTZ", # flaek8-datetimez + "D203", # one blank line before docstring in class + "D212", # docstring on same line as quotes ] # doctests = true # enable when/if available [tool.ruff.lint.per-file-ignores] +"fastnumbers/__init__.py" = [ + "PLC0414", # useless import alias +] +"fastnumbers.pyi" = [ + "A001", # shadowing a builtin + "ANN401", # Any type disallowed + "PLR0913", # too many arguments in function definition +] +"tests/**.py" = [ + "S101", # use of assert + "S110", # log a try/except/pass + "S311", # pseudo-random + "D", # docstring checks + "INP", # implicit namespace package + "PLR2004", # magic value comparison + "PERF", # performance checks +] +"tests/builtin_support.py" = [ + "ANN401", # Any type disallowed +] "tests/test_builtin*.py" = [ - "PT027", # unittest-style assert - "PT009", # unittest-style assert - "PT017", # use pytest.raises + "PT027", # unittest-style assert + "PT009", # unittest-style assert + "PT017", # use pytest.raises + "PLR0915", # too many statements ] -"tests/test_fastnumbers.py"= [ - "PIE794", # class filed defined multiple times +"tests/test_fastnumbers.py" = [ + "ANN401", # Any type disallowed + "ANN002", # need type annotation for *args + "ANN003", # need type annotation for **kwargs + "PLR0913", # too many arguments in function definition + "PIE794", # class filed defined multiple times + "PLR0124", # name compared with itself (used to check for NaN) ] -"fastnumbers.pyi" = [ - "A001", # shadowing a builtin +"tests/test_fastnumbers_examples.py" = [ + "PLR0915", # too many statements +] +"dev/**.py" = [ + "S603", # subprocess call of untrusted input + "S606", # start process without a shell + "S607", # executable with partial path +] +"dev/bump.py" = [ + "T201", # print found +] +"profiling/profile.py" = [ + "ANN", # annotations + "T201", # print found ] [tool.ruff.lint.mccabe] max-complexity = 10 + +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] +known-local-folder = ["builtin_grammar", "builtin_support"] \ No newline at end of file diff --git a/setup.py b/setup.py index dacbdeec..86f28291 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,9 @@ -import glob +"""Rules for compilation of C++ extension module.""" + +from __future__ import annotations + import os +import pathlib import sys from setuptools import Extension, setup @@ -36,8 +40,8 @@ ext = [ Extension( "fastnumbers.fastnumbers", - sorted(glob.glob("src/cpp/*.cpp")), - include_dirs=[os.path.abspath(os.path.join("include"))], + sorted(pathlib.Path("src/cpp").glob("*.cpp")), + include_dirs=[pathlib.Path("include").resolve()], extra_compile_args=compile_args, extra_link_args=["-lm"], ) diff --git a/src/fastnumbers/__init__.py b/src/fastnumbers/__init__.py index 8b473350..787bc1db 100644 --- a/src/fastnumbers/__init__.py +++ b/src/fastnumbers/__init__.py @@ -1,9 +1,12 @@ +"""Public interface for the fastnumbers package.""" + +from __future__ import annotations + from typing import TYPE_CHECKING try: # The redundant "as" tells mypy to treat as explict import - from fastnumbers._version import __version__ as __version__ - from fastnumbers._version import __version_tuple__ as __version_tuple__ + from fastnumbers._version import __version__, __version_tuple__ except ImportError: __version__ = "unknown version" __version_tuple__ = (0, 0, "unknown version") @@ -64,7 +67,7 @@ from typing import Any, Callable, Iterable, NewType, TypeVar, overload IntT = TypeVar("IntT", np.int_) - FloatT = TypeVar("FloatT", np.float_) + FloatT = TypeVar("FloatT", np.float64) CallToInt = Callable[[Any], int] CallToFloat = Callable[[Any], float] ALLOWED_T = NewType("ALLOWED_T", object) @@ -172,8 +175,8 @@ def try_array( ) -> None: ... -def try_array(input, output=None, *, dtype=None, **kwargs): # noqa: A002 - """ +def try_array(input, output=None, *, dtype=None, **kwargs): # noqa: A002, D417 + r""" Quickly convert an iterable's contents into an array. Is basically a direct analogue to using the ``map`` option in :func:`try_float` @@ -266,7 +269,6 @@ def try_array(input, output=None, *, dtype=None, **kwargs): # noqa: A002 Examples -------- - >>> from fastnumbers import try_array >>> import numpy as np >>> try_array(["5", "3", "8"]) diff --git a/tests/builtin_grammar.py b/tests/builtin_grammar.py index 1d8e9ba4..986cba17 100644 --- a/tests/builtin_grammar.py +++ b/tests/builtin_grammar.py @@ -5,6 +5,8 @@ # # Note: since several test cases filter out floats by looking for "e" and ".", # don't add hexadecimal literals that contain "e" or "E". +from __future__ import annotations + VALID_UNDERSCORE_LITERALS = [ "0_0_0", "4_2", diff --git a/tests/builtin_support.py b/tests/builtin_support.py index 10aced77..7d603228 100644 --- a/tests/builtin_support.py +++ b/tests/builtin_support.py @@ -1,8 +1,10 @@ """Supporting definitions for the Python regression tests.""" +from __future__ import annotations + import platform import unittest -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable __all__ = [ "run_with_locale", @@ -26,15 +28,15 @@ def inner(*args: Any, **kwds: Any) -> Any: except AttributeError: # if the test author gives us an invalid category string raise - except: # noqa + except: # noqa: E722 # cannot retrieve original locale, so do nothing - locale = orig_locale = None # type: ignore + locale = orig_locale = None # type: ignore[assignment] else: for loc in locales: try: locale.setlocale(category, loc) break - except: # noqa + except: # noqa: E722 pass # now run the function, resetting the locale on exceptions @@ -66,7 +68,7 @@ def cpython_only(test: Any) -> Any: return impl_detail(cpython=True)(test) -def impl_detail(msg: Optional[str] = None, **guards: bool) -> Callable[[Any], Any]: +def impl_detail(msg: str | None = None, **guards: bool) -> Callable[[Any], Any]: if check_impl_detail(**guards): return _id if msg is None: @@ -79,7 +81,7 @@ def impl_detail(msg: Optional[str] = None, **guards: bool) -> Callable[[Any], An return unittest.skip(msg) -def _parse_guards(guards: Dict[str, bool]) -> Tuple[Dict[str, bool], bool]: +def _parse_guards(guards: dict[str, bool]) -> tuple[dict[str, bool], bool]: # Returns a tuple ({platform_name: run_me}, default_value) if not guards: return ({"cpython": True}, False) diff --git a/tests/conftest.py b/tests/conftest.py index 9e4165c9..687c80a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import hypothesis # This disables the "too slow" hypothesis heath check globally. diff --git a/tests/test_array.py b/tests/test_array.py index 4b27404f..64234460 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -93,7 +93,7 @@ def test_invalid_argument_raises_type_error() -> None: given = [0, 1] with pytest.raises(TypeError, match="got an unexpected keyword argument 'invalid'"): - fastnumbers.try_array(given, invalid="dummy") # type: ignore + fastnumbers.try_array(given, invalid="dummy") # type: ignore[call-overload] @pytest.mark.parametrize( @@ -141,7 +141,7 @@ def test_invalid_input_type_gives_type_error() -> None: expected = "Only numpy ndarray and array.array types for output are " expected += r"supported, not " with pytest.raises(TypeError, match=expected): - fastnumbers.try_array(given, []) # type: ignore + fastnumbers.try_array(given, []) # type: ignore[call-overload] def test_require_output_if_numpy_is_not_installed() -> None: @@ -203,7 +203,7 @@ class TestCPPProtections: def test_non_memorybuffer_type_raises_correct_type_error(self) -> None: """Ensure we only accept well-behaved memory views as input""" with pytest.raises(TypeError, match="not 'list'"): - fastnumbers._array([0, 1], [0, 0]) # type: ignore # noqa: SLF001 + fastnumbers._array([0, 1], [0, 0]) # type: ignore[attr-defined] # noqa: SLF001 @pytest.mark.parametrize("dtype", other_dtypes) def test_invalid_memorybuffer_type_raises_correct_type_error( @@ -214,7 +214,7 @@ def test_invalid_memorybuffer_type_raises_correct_type_error( output = np.array([0, 0], dtype=dtype) exception = r"Unknown buffer format '\S+' for object" with pytest.raises(TypeError, match=exception): - fastnumbers._array(given, output) # type: ignore # noqa: SLF001 + fastnumbers._array(given, output) # type: ignore[attr-defined] # noqa: SLF001 kwargs = ["inf", "nan", "on_fail", "on_overflow", "on_type_error"] @@ -241,7 +241,7 @@ def test_string_replacement_type_gives_type_error( expected = f"The default value of 'not ok' given to option '{kwarg}' " expected += "has type 'str' which cannot be converted to a numeric value" with pytest.raises(TypeError, match=expected): - fastnumbers.try_array(given, result, **{kwarg: "not ok"}) # type: ignore + fastnumbers.try_array(given, result, **{kwarg: "not ok"}) # type: ignore[call-overload] @pytest.mark.parametrize("data_type", int_data_types) @pytest.mark.parametrize("kwarg", kwargs) @@ -253,7 +253,7 @@ def test_float_replacement_type_for_int_gives_value_error( expected = rf"The default value of 1\.3 given to option '{kwarg}' " expected += f"cannot be converted to C type '{data_type}'" with pytest.raises(ValueError, match=expected): - fastnumbers.try_array(given, result, **{kwarg: 1.3}) # type: ignore + fastnumbers.try_array(given, result, **{kwarg: 1.3}) # type: ignore[call-overload] @pytest.mark.parametrize("data_type", float_data_types) @pytest.mark.parametrize( @@ -417,7 +417,10 @@ class TestErrors: @pytest.mark.parametrize("data_type", data_types) @pytest.mark.parametrize("style", [list, iter]) def test_given_junk_float_type_raises_error( - self, data_type: str, dumb: Any, style: Callable[[Any], Any] + self, + data_type: str, + dumb: DumbFloatClass | DumbIntClass, + style: Callable[[Any], Any], ) -> None: given = style([dumb]) result = array.array(formats[data_type], [0]) @@ -469,7 +472,7 @@ def broken() -> Iterator[str]: def test_given_non_iterable_raises_type_error(self, data_type: str) -> None: output = array.array(formats[data_type], [0, 0, 0, 0]) with pytest.raises(TypeError, match="'int' object is not iterable"): - fastnumbers.try_array(5, output) # type: ignore + fastnumbers.try_array(5, output) # type: ignore[call-overload] @pytest.mark.parametrize("data_type", data_types) @pytest.mark.parametrize("style", [list, iter]) diff --git a/tests/test_builtin_float.py b/tests/test_builtin_float.py index e790b434..4c5a2080 100644 --- a/tests/test_builtin_float.py +++ b/tests/test_builtin_float.py @@ -1,19 +1,24 @@ +from __future__ import annotations + +import ast import builtins import sys import time import unittest from math import copysign, isinf, isnan -from typing import Callable, List, Union +from typing import Callable -import builtin_support as support import pytest +from typing_extensions import Self + +from fastnumbers import float + +import builtin_support as support from builtin_grammar import ( INVALID_UNDERSCORE_LITERALS, VALID_UNDERSCORE_LITERALS, ) -from fastnumbers import float - INF = float("inf") NAN = float("nan") @@ -68,7 +73,7 @@ def test_noargs(self) -> None: def test_underscores(self) -> None: for lit in VALID_UNDERSCORE_LITERALS: if not any(ch in lit for ch in "jJxXoObB"): - assert float(lit) == eval(lit) + assert float(lit) == ast.literal_eval(lit) assert float(lit) == float(lit.replace("_", "")) for lit in INVALID_UNDERSCORE_LITERALS: if lit in ("0_7", "09_99"): # octals are not recognized here @@ -89,7 +94,7 @@ def test_underscores(self) -> None: def test_non_numeric_input_types(self) -> None: # Test possible non-numeric types for the argument x, including # subclasses of the explicitly documented accepted types. - class CustomStr(str): + class CustomStr(str): # noqa: SLOT000 pass class CustomBytes(bytes): @@ -98,7 +103,7 @@ class CustomBytes(bytes): class CustomByteArray(bytearray): pass - factories: List[Callable[[bytes], Union[bytes, bytearray, str]]] = [ + factories: list[Callable[[bytes], bytes | bytearray | str]] = [ bytes, bytearray, lambda b: CustomStr(b.decode()), @@ -111,7 +116,7 @@ class CustomByteArray(bytearray): except ImportError: pass else: - factories.append(lambda b: array("B", b)) # type: ignore + factories.append(lambda b: array("B", b)) # type: ignore[return-value,arg-type] for f in factories: x = f(b" 3.14 ") @@ -138,7 +143,7 @@ def test_error_message_old(self) -> None: self.fail(f"Expected int({s!r}) to raise a ValueError") def test_error_message(self) -> None: - def check(s: Union[str, bytes]) -> None: + def check(s: str | bytes) -> None: with self.assertRaises(ValueError, msg=f"float({s!r})") as cm: float(s) assert str(cm.exception) == f"could not convert string to float: {s!r}" @@ -202,7 +207,7 @@ def __float__(self) -> builtins.float: return 42.0 class Foo3(builtins.float): - def __new__(cls, value: builtins.float = 0.0) -> "Foo3": + def __new__(cls, value: builtins.float = 0.0) -> Self: return builtins.float.__new__(cls, 2 * value) def __float__(self) -> builtins.float: @@ -214,7 +219,7 @@ def __float__(self) -> int: # Issue 5759: __float__ not called on str subclasses (though it is on # unicode subclasses). - class FooStr(str): + class FooStr(str): # noqa: SLOT000 def __float__(self) -> builtins.float: return float(str(self)) + 1 diff --git a/tests/test_builtin_int.py b/tests/test_builtin_int.py index 8eb5e670..547c1563 100644 --- a/tests/test_builtin_int.py +++ b/tests/test_builtin_int.py @@ -1,18 +1,22 @@ +from __future__ import annotations + +import ast import builtins import contextlib import sys import unittest -from typing import Callable, List, Optional, Union +from typing import Callable -import builtin_support as support import pytest + +from fastnumbers import int + +import builtin_support as support from builtin_grammar import ( INVALID_UNDERSCORE_LITERALS, VALID_UNDERSCORE_LITERALS, ) -from fastnumbers import int - L = [ ("0", 0), ("1", 1), @@ -61,7 +65,7 @@ def test_basic(self) -> None: ss = prefix + sign + s vv = v if sign == "-" and v is not ValueError: - vv = -v # type: ignore + vv = -v # type: ignore[operator] with contextlib.suppress(ValueError): assert int(ss) == vv @@ -223,7 +227,7 @@ def test_underscores(self) -> None: for lit in VALID_UNDERSCORE_LITERALS: if any(ch in lit for ch in ".eEjJ"): continue - assert int(lit, 0) == eval(lit) + assert int(lit, 0) == ast.literal_eval(lit) assert int(lit, 0) == int(lit.replace("_", ""), 0) for lit in INVALID_UNDERSCORE_LITERALS: if any(ch in lit for ch in ".eEjJ"): @@ -279,9 +283,9 @@ def test_int_base_limits(self) -> None: def test_int_base_bad_types(self) -> None: """Not integer types are not valid bases; issue16772.""" with pytest.raises(TypeError): - int("0", 5.5) # type: ignore + int("0", 5.5) # type: ignore[call-overload] with pytest.raises(TypeError): - int("0", 5.0) # type: ignore + int("0", 5.0) # type: ignore[call-overload] def test_int_base_indexable(self) -> None: class MyIndexable: @@ -304,7 +308,7 @@ def __index__(self) -> builtins.int: def test_non_numeric_input_types(self) -> None: # Test possible non-numeric types for the argument x, including # subclasses of the explicitly documented accepted types. - class CustomStr(str): + class CustomStr(str): # noqa: SLOT000 pass class CustomBytes(bytes): @@ -313,7 +317,7 @@ class CustomBytes(bytes): class CustomByteArray(bytearray): pass - factories: List[Callable[[bytes], Union[bytes, bytearray, str]]] = [ + factories: list[Callable[[bytes], bytes | bytearray | str]] = [ bytes, bytearray, lambda b: CustomStr(b.decode()), @@ -326,7 +330,7 @@ class CustomByteArray(bytearray): except ImportError: pass else: - factories.append(lambda b: array("B", b)) # type: ignore + factories.append(lambda b: array("B", b)) # type: ignore[arg-type,return-value] for f in factories: x = f(b"100") @@ -374,7 +378,7 @@ class Classic: for base in (object, Classic): - class IntOverridesTrunc(base): # type: ignore + class IntOverridesTrunc(base): # type: ignore[misc,valid-type] def __int__(self) -> builtins.int: return 42 @@ -390,8 +394,8 @@ def __index__(self) -> builtins.int: return 42 class BadIndex(builtins.int): - def __index__(self) -> builtins.float: # type: ignore - return 42.0 + def __index__(self) -> builtins.float: # type: ignore[override] + return 42.0 # noqa: PLE0305 my_int = MyIndex(7) assert my_int == 7 @@ -405,10 +409,10 @@ def __int__(self) -> builtins.int: return 42 class BadInt(builtins.int): - def __int__(self) -> builtins.float: # type: ignore + def __int__(self) -> builtins.float: # type: ignore[override] return 42.0 - my_int: Union[MyInt, BadInt] = MyInt(7) + my_int: MyInt | BadInt = MyInt(7) assert my_int == 7 assert int(my_int) == 42 @@ -419,11 +423,11 @@ def __int__(self) -> builtins.float: # type: ignore def test_int_returns_int_subclass(self) -> None: class BadIndex: def __index__(self) -> bool: - return True + return True # noqa: PLE0305 class BadIndex2(builtins.int): def __index__(self) -> bool: - return True + return True # noqa: PLE0305 class BadInt: def __int__(self) -> bool: @@ -433,7 +437,7 @@ class BadInt2(builtins.int): def __int__(self) -> bool: return True - bad_int: Union[BadInt, BadIndex, BadIndex2] + bad_int: BadInt | BadIndex | BadIndex2 bad_int = BadIndex() with self.assertWarns(DeprecationWarning): n = int(bad_int) @@ -458,7 +462,7 @@ def __int__(self) -> bool: assert type(n) is builtins.int def test_error_message(self) -> None: - def check(s: Union[str, bytes], base: Optional[builtins.int] = None) -> None: + def check(s: str | bytes, base: builtins.int | None = None) -> None: with self.assertRaises(ValueError, msg=f"int({s!r}, {base!r})") as cm: if base is None: int(s) diff --git a/tests/test_fastnumbers.py b/tests/test_fastnumbers.py index 8483bd8a..ad5bc1bd 100644 --- a/tests/test_fastnumbers.py +++ b/tests/test_fastnumbers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import decimal import math import random @@ -8,12 +10,9 @@ from typing import ( Any, Callable, - Dict, Iterable, Iterator, - List, NoReturn, - Tuple, Union, cast, ) @@ -141,7 +140,7 @@ def __call__( class Real(Protocol): - def __call__(self, x: Any = ..., *, coerce: bool = ...) -> Union[int, float]: ... + def __call__(self, x: Any = ..., *, coerce: bool = ...) -> int | float: ... class FastReal(Protocol): @@ -276,7 +275,7 @@ def __call__( ] -def a_number(s: Union[str, bytes]) -> bool: +def a_number(s: str | bytes) -> bool: s = s.strip() try: int(s) @@ -291,9 +290,9 @@ def a_number(s: Union[str, bytes]) -> bool: return True if isinstance(s, bytes): return False - if re.match(r"\s*([-+]?\d+\.?\d*(?:[eE][-+]?\d+)?)\s*$", s, re.U): + if re.match(r"\s*([-+]?\d+\.?\d*(?:[eE][-+]?\d+)?)\s*$", s, re.UNICODE): return True - if re.match(r"\s*([-+]?\.\d+(?:[eE][-+]?\d+)?)\s*$", s, re.U): + if re.match(r"\s*([-+]?\.\d+(?:[eE][-+]?\d+)?)\s*$", s, re.UNICODE): return True return s.strip().lstrip("[-+]") in numeric @@ -308,7 +307,7 @@ def pad(value: str) -> str: return random.randint(1, 100) * space() + value + random.randint(1, 100) * space() -def not_a_number(s: Union[str, bytes]) -> bool: +def not_a_number(s: str | bytes) -> bool: return not a_number(s) @@ -321,7 +320,7 @@ def not_an_integer(x: float) -> bool: def capture_result( # type: ignore [no-untyped-def] - func: Union[ConversionFuncs, IdentificationFuncs], *args, **kwargs + func: ConversionFuncs | IdentificationFuncs, *args, **kwargs ) -> Any: """Execute a function, and either return the result or the exception message""" try: @@ -368,7 +367,7 @@ def __int__(self) -> NoReturn: # Map function names to the actual functions, # for dymamic declaration of which functions test below. -func_mapping: Dict[str, Callable[..., Any]] = { +func_mapping: dict[str, Callable[..., Any]] = { "check_real": fastnumbers.check_real, "check_float": fastnumbers.check_float, "check_int": fastnumbers.check_int, @@ -396,7 +395,7 @@ def __int__(self) -> NoReturn: } -def get_funcs(function_names: Iterable[str]) -> List[Callable[..., Any]]: +def get_funcs(function_names: Iterable[str]) -> list[Callable[..., Any]]: """Given a list of function names, return the associated functions""" return [func_mapping[x] for x in function_names] @@ -451,16 +450,16 @@ def test_real_no_arguments_returns_0(self) -> None: @parametrize("func", get_funcs(funcs), ids=funcs) def test_invalid_argument_raises_type_error( - self, func: Union[Callable[[Any], Any], Real] + self, func: Callable[[Any], Any] | Real ) -> None: with pytest.raises( TypeError, match="got an unexpected keyword argument 'invalid'" ): - func(5, invalid="dummy") # type: ignore + func(5, invalid="dummy") # type: ignore[call-arg] def test_invalid_argument_raises_type_error_no_kwargs(self) -> None: with pytest.raises(TypeError, match="takes no keyword arguments"): - fastnumbers.float("5", invalid="dummy") # type: ignore + fastnumbers.float("5", invalid="dummy") # type: ignore[call-arg] funcs = [ *non_builtin_funcs, @@ -478,11 +477,11 @@ def test_invalid_argument_raises_type_error_no_kwargs(self) -> None: @parametrize("func", get_funcs(funcs), ids=funcs) def test_no_arguments_raises_type_error(self, func: Callable[[Any], Any]) -> None: with pytest.raises(TypeError, match="missing required argument 'x'"): - func() # type: ignore + func() # type: ignore[call-arg] def test_invalid_argument_and_missing_positional(self) -> None: with pytest.raises(TypeError, match="missing required argument 'x'"): - fastnumbers.try_float(on_fail=0.0) # type: ignore + fastnumbers.try_float(on_fail=0.0) # type: ignore[call-overload] def test_positional_as_kwarg_is_ok(self) -> None: assert fastnumbers.try_float(x="5") == 5.0 @@ -490,17 +489,17 @@ def test_positional_as_kwarg_is_ok(self) -> None: def test_duplicate_argument(self) -> None: msg = r"argument for .* given by name \('x'\) and position" with pytest.raises(TypeError, match=msg): - fastnumbers.try_float("5", x="4") # type: ignore + fastnumbers.try_float("5", x="4") # type: ignore[call-overload] def test_too_many_positional_arguments1(self) -> None: msg = r"takes 1 positional arguments but 2 were given" with pytest.raises(TypeError, match=msg): - fastnumbers.try_float("5", "4") # type: ignore + fastnumbers.try_float("5", "4") # type: ignore[call-overload] def test_too_many_positional_arguments2(self) -> None: msg = r"takes from 0 to 1 positional arguments but 2 were given" with pytest.raises(TypeError, match=msg): - fastnumbers.float("5", "4") # type: ignore + fastnumbers.float("5", "4") # type: ignore[call-arg] class TestSelectors: @@ -531,7 +530,7 @@ def test_selectors_are_mutually_exclusive(self, a: object, b: object) -> None: [fastnumbers.DISALLOWED, fastnumbers.STRING_ONLY, fastnumbers.NUMBER_ONLY], ) def test_selectors_are_rejected_when_invalid_inf_conv( - self, func: Union[TryReal, TryFloat], inf: object + self, func: TryReal | TryFloat, inf: object ) -> None: with pytest.raises(ValueError, match="'inf' and 'nan' cannot be"): func("5", inf=inf) @@ -542,7 +541,7 @@ def test_selectors_are_rejected_when_invalid_inf_conv( [fastnumbers.DISALLOWED, fastnumbers.STRING_ONLY, fastnumbers.NUMBER_ONLY], ) def test_selectors_are_rejected_when_invalid_nan_conv( - self, func: Union[TryReal, TryFloat], nan: object + self, func: TryReal | TryFloat, nan: object ) -> None: with pytest.raises(ValueError, match="'inf' and 'nan' cannot be"): func("5", nan=nan) @@ -555,7 +554,7 @@ def test_selectors_are_rejected_when_invalid_nan_conv( [fastnumbers.INPUT, fastnumbers.RAISE, str, float, int], ) def test_selectors_are_rejected_when_invalid_inf_check( - self, func: Union[CheckReal, CheckFloat], inf: object + self, func: CheckReal | CheckFloat, inf: object ) -> None: with pytest.raises(ValueError, match="allowed values for 'inf' and 'nan'"): func("5", inf=inf) @@ -566,7 +565,7 @@ def test_selectors_are_rejected_when_invalid_inf_check( [fastnumbers.INPUT, fastnumbers.RAISE, str, float, int], ) def test_selectors_are_rejected_when_invalid_nan_check( - self, func: Union[CheckReal, CheckFloat], nan: object + self, func: CheckReal | CheckFloat, nan: object ) -> None: with pytest.raises(ValueError, match="allowed values for 'inf' and 'nan'"): func("5", nan=nan) @@ -653,7 +652,7 @@ def test_new_invalid_parings(self, func: Any) -> None: func("5", raise_on_invalid=True, on_fail=0.0) old_to_new_conversion_pairing = [] - conv_pairs: List[Tuple[OldConversionFuncs, ConversionFuncs]] = [ + conv_pairs: list[tuple[OldConversionFuncs, ConversionFuncs]] = [ (partial(fastnumbers.fast_real, allow_underscores=False), fastnumbers.try_real), ( partial(fastnumbers.fast_float, allow_underscores=False), @@ -697,7 +696,7 @@ def test_old_to_new_conversion_equivalence( assert old_result == new_result old_to_new_checking_pairing = [] - check_pairs: List[Tuple[OldIdentificationFuncs, IdentificationFuncs]] = [ + check_pairs: list[tuple[OldIdentificationFuncs, IdentificationFuncs]] = [ (partial(fastnumbers.isreal, allow_underscores=False), fastnumbers.check_real), ( partial(fastnumbers.isfloat, allow_underscores=False), @@ -826,20 +825,20 @@ class TestErrorHandlingConversionFunctionsSuccessful: funcs = ["try_real", "try_float"] @parametrize("func", get_funcs(funcs), ids=funcs) - def test_given_nan_returns_nan(self, func: Union[TryReal, TryFloat]) -> None: + def test_given_nan_returns_nan(self, func: TryReal | TryFloat) -> None: assert math.isnan(func(float("nan"))) assert math.isnan(func(float("nan"), nan=fastnumbers.ALLOWED)) # default @parametrize("func", get_funcs(funcs), ids=funcs) @parametrize("x", [*all_nan, pad("nan"), pad("-NAN")]) def test_given_nan_string_returns_nan( - self, func: Union[TryReal, TryFloat], x: str + self, func: TryReal | TryFloat, x: str ) -> None: assert math.isnan(func(x)) assert math.isnan(func(x, nan=fastnumbers.ALLOWED)) # default @parametrize("func", get_funcs(funcs), ids=funcs) - def test_given_nan_returns_sub_value(self, func: Union[TryReal, TryFloat]) -> None: + def test_given_nan_returns_sub_value(self, func: TryReal | TryFloat) -> None: assert func(float("nan"), nan=0) == 0 assert math.isnan(func(float("nan"), nan=fastnumbers.INPUT)) with pytest.raises(ValueError): @@ -848,7 +847,7 @@ def test_given_nan_returns_sub_value(self, func: Union[TryReal, TryFloat]) -> No @parametrize("func", get_funcs(funcs), ids=funcs) def test_with_nan_given_nan_string_returns_sub_value( - self, func: Union[TryReal, TryFloat] + self, func: TryReal | TryFloat ) -> None: assert func("nan", nan=0.0) == 0.0 assert func("nan", nan=fastnumbers.INPUT) == "nan" @@ -857,14 +856,14 @@ def test_with_nan_given_nan_string_returns_sub_value( assert func("nan", nan=lambda _: "hello") == "hello" @parametrize("func", get_funcs(funcs), ids=funcs) - def test_given_inf_returns_inf(self, func: Union[TryReal, TryFloat]) -> None: + def test_given_inf_returns_inf(self, func: TryReal | TryFloat) -> None: assert math.isinf(func(float("inf"))) assert math.isinf(func(float("inf"), inf=fastnumbers.ALLOWED)) # default @parametrize("func", get_funcs(funcs), ids=funcs) @parametrize("x", [*most_inf, pad("inf"), pad("+INFINITY")]) def test_given_inf_string_returns_inf( - self, func: Union[TryReal, TryFloat], x: str + self, func: TryReal | TryFloat, x: str ) -> None: assert math.isinf(func(x)) assert math.isinf(func(float("inf"), inf=fastnumbers.ALLOWED)) # default @@ -872,12 +871,12 @@ def test_given_inf_string_returns_inf( @parametrize("func", get_funcs(funcs), ids=funcs) @parametrize("x", [*neg_inf, pad("-inf"), pad("-INFINITY")]) def test_given_negative_inf_string_returns_negative_inf( - self, func: Union[TryReal, TryFloat], x: str + self, func: TryReal | TryFloat, x: str ) -> None: assert func(x) == float("-inf") @parametrize("func", get_funcs(funcs), ids=funcs) - def test_given_inf_returns_sub_value(self, func: Union[TryReal, TryFloat]) -> None: + def test_given_inf_returns_sub_value(self, func: TryReal | TryFloat) -> None: assert func(float("inf"), inf=1000.0) == 1000.0 assert math.isinf(func(float("inf"), inf=fastnumbers.INPUT)) with pytest.raises(ValueError): @@ -886,7 +885,7 @@ def test_given_inf_returns_sub_value(self, func: Union[TryReal, TryFloat]) -> No @parametrize("func", get_funcs(funcs), ids=funcs) def test_with_inf_given_inf_string_returns_sub_value( - self, func: Union[TryReal, TryFloat] + self, func: TryReal | TryFloat ) -> None: assert func("inf", inf=10000.0) == 10000.0 assert func("-inf", inf=10000.0) == 10000.0 @@ -902,7 +901,7 @@ def test_with_inf_given_inf_string_returns_sub_value( @given(floats(allow_nan=False)) @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_float_returns_float( - self, func: Union[TryReal, TryFloat], x: float + self, func: TryReal | TryFloat, x: float ) -> None: result = func(x) assert result == x @@ -913,7 +912,7 @@ def test_given_float_returns_float( @example("10." + "0" * 1050) # absurdly large number of zeros @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_float_string_returns_float( - self, func: Union[TryReal, TryFloat], x: str + self, func: TryReal | TryFloat, x: str ) -> None: expected = float(x) result = func(x) @@ -926,7 +925,7 @@ def test_given_float_string_returns_float( @given(floats(allow_nan=False, allow_infinity=False)) @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_float_returns_int( - self, func: Union[TryInt, TryForceInt], x: float + self, func: TryInt | TryForceInt, x: float ) -> None: expected = int(x) result = func(x) @@ -946,7 +945,7 @@ def test_given_float_returns_int( @example(int(10 * 300)) @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_int_returns_int( - self, func: Union[TryReal, TryInt, TryForceInt], x: int + self, func: TryReal | TryInt | TryForceInt, x: int ) -> None: result = func(x) assert result == x @@ -970,7 +969,7 @@ def test_given_int_returns_int( @example("33684944745210074227862907273261282807602986571245071790093633147269") @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_int_string_returns_int( - self, func: Union[TryReal, TryInt, TryForceInt], x: str + self, func: TryReal | TryInt | TryForceInt, x: str ) -> None: expected = int(x) result = func(x) @@ -990,7 +989,7 @@ def test_given_int_string_returns_int( @given(sampled_from(digits)) @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_unicode_digit_returns_int( - self, func: Union[TryReal, TryInt, TryForceInt], x: str + self, func: TryReal | TryInt | TryForceInt, x: str ) -> None: expected = unicodedata.digit(x) result = func(x) @@ -1004,7 +1003,7 @@ def test_given_unicode_digit_returns_int( @example("\u0f33") # the only negative unicode character @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_unicode_numeral_returns_float( - self, func: Union[TryReal, TryInt, TryForceInt], x: str + self, func: TryReal | TryInt | TryForceInt, x: str ) -> None: expected = unicodedata.numeric(x) result = func(x) @@ -1064,7 +1063,7 @@ class TestErrorHandlingConversionFunctionsUnsucessful: @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_dumb_float_class_responds_to_internal_valueerror( - self, func: Union[TryReal, TryFloat] + self, func: TryReal | TryFloat ) -> None: x = DumbFloatClass() assert func(x) is x @@ -1076,7 +1075,7 @@ def test_given_dumb_float_class_responds_to_internal_valueerror( @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_dumb_int_class_responds_to_internal_valueerror( - self, func: Union[TryInt, TryForceInt] + self, func: TryInt | TryForceInt ) -> None: x = DumbIntClass() assert func(x) is x @@ -1143,14 +1142,14 @@ def test_given_invalid_type_returns_sub_with_on_type_error( @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_nan_raises_valueerror_for_int_funcions( - self, func: Union[TryInt, TryForceInt] + self, func: TryInt | TryForceInt ) -> None: with pytest.raises(ValueError): func(float("nan"), on_fail=fastnumbers.RAISE) @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_inf_raises_overflowerror_for_int_funcions( - self, func: Union[TryInt, TryForceInt] + self, func: TryInt | TryForceInt ) -> None: with pytest.raises(OverflowError): func(float("inf"), on_fail=fastnumbers.RAISE) @@ -1426,7 +1425,7 @@ class TestCheckingFunctions: @parametrize("func", get_funcs(funcs), ids=funcs) @parametrize("x", [float("nan"), float("inf"), float("-inf")]) def test_returns_true_for_nan_and_inf( - self, func: Union[CheckReal, CheckFloat], x: float + self, func: CheckReal | CheckFloat, x: float ) -> None: assert func(x) assert func(x, nan=fastnumbers.NUMBER_ONLY, inf=fastnumbers.NUMBER_ONLY) @@ -1437,7 +1436,7 @@ def test_returns_true_for_nan_and_inf( @parametrize("func", get_funcs(funcs), ids=funcs) @parametrize("x", [*all_nan, pad("nan"), pad("-NAN")]) def test_returns_false_for_nan_string_unless_allow_nan_is_true( - self, func: Union[CheckReal, CheckFloat], x: str + self, func: CheckReal | CheckFloat, x: str ) -> None: assert not func(x) assert not func(x, nan=fastnumbers.NUMBER_ONLY) # default @@ -1448,7 +1447,7 @@ def test_returns_false_for_nan_string_unless_allow_nan_is_true( @parametrize("func", get_funcs(funcs), ids=funcs) @parametrize("x", most_inf + neg_inf + [pad("-inf"), pad("+INFINITY")]) def test_returns_false_for_inf_string_unless_allow_infinity_is_true( - self, func: Union[CheckReal, CheckFloat], x: str + self, func: CheckReal | CheckFloat, x: str ) -> None: assert not func(x) assert not func(x, nan=fastnumbers.NUMBER_ONLY) # default @@ -1463,7 +1462,7 @@ def test_returns_false_for_inf_string_unless_allow_infinity_is_true( @given(integers()) @parametrize("func", get_funcs(funcs), ids=funcs) def test_returns_true_if_given_int( - self, func: Union[CheckReal, CheckInt, CheckIntLike], x: int + self, func: CheckReal | CheckInt | CheckIntLike, x: int ) -> None: assert func(x) assert func(x, consider=None) # default @@ -1474,7 +1473,7 @@ def test_returns_true_if_given_int( @given(floats()) @parametrize("func", get_funcs(funcs), ids=funcs) def test_returns_true_if_given_float( - self, func: Union[CheckReal, CheckFloat], x: float + self, func: CheckReal | CheckFloat, x: float ) -> None: assert func(x) assert func(x, consider=None) # default @@ -1497,7 +1496,7 @@ def test_returns_false_if_given_number_and_str_only_is_true( @example("10." + "0" * 1050) @parametrize("func", get_funcs(funcs), ids=funcs) def test_returns_true_if_given_float_string( - self, func: Union[CheckReal, CheckFloat], x: str + self, func: CheckReal | CheckFloat, x: str ) -> None: assert func(x) assert func(pad(x)) # Accepts padding @@ -1544,7 +1543,7 @@ def test_given_unicode_digit_returns_true( @given(sampled_from(numeric_not_digit_not_int)) @parametrize("func", get_funcs(funcs), ids=funcs) def test_given_unicode_numeral_returns_true( - self, func: Union[CheckReal, CheckFloat], x: str + self, func: CheckReal | CheckFloat, x: str ) -> None: assert func(x) assert func(pad(x)) # Accepts padding @@ -1585,7 +1584,7 @@ def test_given_unicode_of_more_than_one_char_returns_false( @parametrize("func", get_funcs(funcs), ids=funcs) def test_returns_false_for_nan_or_inf_string( - self, func: Union[CheckInt, CheckIntLike] + self, func: CheckInt | CheckIntLike ) -> None: assert not func("nan") assert not func("inf") @@ -1727,7 +1726,7 @@ class TestQueryType: def test_allowed_type_must_be_a_sequence(self) -> None: with pytest.raises(TypeError, match="allowed_type is not a sequence type"): - fastnumbers.query_type("5", allowed_types={str: float}) # type: ignore + fastnumbers.query_type("5", allowed_types={str: float}) # type: ignore[call-overload] def test_allowed_type_must_non_empty(self) -> None: with pytest.raises( @@ -1852,7 +1851,7 @@ def test_given_float_returns_float_or_int_with_coerce(self, x: float) -> None: | iterables(floats()) ) def test_containers_returns_container_type( - self, x: Union[Dict[float, float], Iterable[float]] + self, x: dict[float, float] | Iterable[float] ) -> None: assert fastnumbers.query_type(x) is type(x) assert fastnumbers.query_type(x, allowed_types=(float, int, str)) is None @@ -1886,8 +1885,8 @@ def test_mapping_non_mapping_behave_the_same( self, nomapper: ConversionFuncs, mapper: ConversionFuncs, - kwargs: Dict[str, Any], - x: List[Union[float, int, str]], + kwargs: dict[str, Any], + x: list[float | int | str], ) -> None: nomapper = partial(nomapper, **kwargs) mapper = partial(mapper, **kwargs) @@ -1973,17 +1972,11 @@ def test_mapping_handles_range(self, func: ConversionFuncs) -> None: def test_mapping_iterator_handles_range(self, func: ConversionFuncs) -> None: """Range is a sequence but is not a 'fast sequence'""" result = func(range(4)) - print(1) assert next(result) == 0 - print(2) assert next(result) == 1 - print(3) assert next(result) == 2 - print(4) assert next(result) == 3 - print(5) assert next(result, None) is None - print(6) @parametrize( "func", @@ -2034,7 +2027,7 @@ def broken() -> Iterator[str]: def test_mapping_handles_empty_iterable( self, func: ConversionFuncs, iterable_gen: Callable[[], Iterable[Any]] ) -> None: - expected: List[Any] = [] + expected: list[Any] = [] result = func(iterable_gen()) assert result == expected diff --git a/tests/test_fastnumbers_examples.py b/tests/test_fastnumbers_examples.py index 23626a1c..8c5da973 100644 --- a/tests/test_fastnumbers_examples.py +++ b/tests/test_fastnumbers_examples.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math from typing import cast @@ -59,7 +61,7 @@ def test_try_real() -> None: assert isinstance(fastnumbers.try_real("4029.0", coerce=False), float) # 11. TypeError for invalid input with pytest.raises(TypeError): - fastnumbers.try_real(["hey"]) # type: ignore + fastnumbers.try_real(["hey"]) # type: ignore[call-overload] # 12. Invalid input string assert fastnumbers.try_real("not_a_number") == "not_a_number" with pytest.raises(ValueError): @@ -132,7 +134,7 @@ def test_try_float() -> None: assert isinstance(fastnumbers.try_float("4029"), float) # 11. TypeError for invalid input with pytest.raises(TypeError): - fastnumbers.try_float(["hey"]) # type: ignore + fastnumbers.try_float(["hey"]) # type: ignore[call-overload] # 12. Invalid input string assert fastnumbers.try_float("not_a_number") == "not_a_number" with pytest.raises(ValueError): @@ -197,7 +199,7 @@ def test_try_int() -> None: assert isinstance(fastnumbers.try_int(4029.00), int) # 11. TypeError for invalid input with pytest.raises(TypeError): - fastnumbers.try_int(["hey"]) # type: ignore + fastnumbers.try_int(["hey"]) # type: ignore[call-overload] # 12. Invalid input string assert fastnumbers.try_int("not_a_number") == "not_a_number" with pytest.raises(ValueError): @@ -259,7 +261,7 @@ def test_try_forceint() -> None: assert isinstance(fastnumbers.try_forceint("4029.00"), int) # 11. TypeError for invalid input with pytest.raises(TypeError): - fastnumbers.try_forceint(["hey"]) # type: ignore + fastnumbers.try_forceint(["hey"]) # type: ignore[call-overload] # 12. Invalid input string assert fastnumbers.try_forceint("not_a_number") == "not_a_number" with pytest.raises(ValueError): diff --git a/tox.ini b/tox.ini index a5706175..e8041a71 100644 --- a/tox.ini +++ b/tox.ini @@ -66,8 +66,9 @@ deps = hypothesis pytest numpy + setuptools_scm commands = - mypy --strict tests + mypy --strict tests dev # Build documentation. # sphinx and sphinx_rtd_theme not in docs/requirements.txt because they