diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 75406db6..d83f0095 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -12,7 +12,7 @@ jobs: backport: name: Backport PR if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport')) && !(contains(github.event.pull_request.labels.*.name, 'backport-pr-created')) - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: # Workaround not to fail the workflow if the PR does not need a backport # https://github.com/sorenlouv/backport-github-action/issues/127#issuecomment-2277248320 @@ -25,7 +25,7 @@ jobs: echo "matched=$matched" >> $GITHUB_OUTPUT - name: Backport Action if: fromJSON(steps.check_labels.outputs.matched) > 0 - uses: sorenlouv/backport-github-action@v12.0.4 + uses: sorenlouv/backport-github-action@v12 with: github_token: ${{ secrets.GITHUB_TOKEN }} auto_backport_label_prefix: backport-to- diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 37f53504..e5cbc908 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -8,7 +8,7 @@ jobs: CFLAGS: -Wall -Werror -std=c17 -Wconversion strategy: matrix: - os: [ubuntu-24.04] + os: [ubuntu-26.04] python-version: [pypy3.12, "graalpy-25", "3.x"] steps: - uses: actions/checkout@v7 @@ -55,7 +55,7 @@ jobs: build/coverage-${{ matrix.python-version }}.info retention-days: 1 merge-coverage: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 needs: - coverage steps: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 82d16bd4..f8c7fa73 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -2,7 +2,7 @@ name: Run pre-commit on: [workflow_dispatch, workflow_call] jobs: linter: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 03bfe02a..773be19d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,7 @@ jobs: build: name: Build distribution if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7 with: @@ -38,7 +38,7 @@ jobs: needs: - build - wheels - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 environment: name: pypi url: https://pypi.org/p/python-gmp @@ -57,7 +57,7 @@ jobs: and upload them to GitHub Release needs: - publish-to-pypi - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: write id-token: write diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1b080036..5ca80ff5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -6,19 +6,19 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-24.04, ubuntu-24.04-arm, macos-26-intel, macos-26, - windows-2025-vs2026] + os: [ubuntu-26.04, ubuntu-26.04-arm, macos-26-intel, macos-26, + windows-2025] msystem: [ucrt64] menv: [ucrt-x86_64] include: - - os: windows-11-arm + - os: windows-11-vs2026-arm msystem: clangarm64 menv: clang-aarch64 steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: msys2/setup-msys2@v2.33.0 + - uses: msys2/setup-msys2@v2 name: Setup msys2 with: install: >- @@ -42,13 +42,13 @@ jobs: - run: echo "PKG_CONFIG_PATH=${{ github.workspace }}/.local/lib/pkgconfig" >> $env:GITHUB_ENV if: ${{ startsWith(matrix.os, 'windows') }} - name: Build wheels - uses: pypa/cibuildwheel@v4.3.0 + uses: pypa/cibuildwheel@v4.3.1 - uses: actions/upload-artifact@v7 with: name: wheels-${{ matrix.os }} path: ./wheelhouse/ merge: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 needs: build_wheels steps: - name: Merge Wheels diff --git a/bench/pidigits.py b/bench/pidigits.py new file mode 100644 index 00000000..130fe094 --- /dev/null +++ b/bench/pidigits.py @@ -0,0 +1,70 @@ +""" +Calculating some of the digits of π. + +This benchmark stresses big integer arithmetic. + +Adapted from code on: +http://benchmarksgame.alioth.debian.org/ +""" + +import itertools + +import pyperf + +from bench.utils import mpz + +DEFAULT_DIGITS = 2000 +icount = itertools.count +islice = itertools.islice + + +def gen_x(): + return map(lambda k: (k, 4 * k + 2, 0, 2 * k + 1), icount(1)) + + +def compose(a, b): + aq, ar, as_, at = a + bq, br, bs, bt = b + return (aq * bq, + aq * br + ar * bt, + as_ * bq + at * bs, + as_ * br + at * bt) + + +def extract(z, j): + q, r, s, t = z + return (q * j + r) // (s * j + t) + + +def gen_pi_digits(): + z = (mpz(1), mpz(0), mpz(0), mpz(1)) + x = gen_x() + while 1: + y = extract(z, 3) + while y != extract(z, 4): + z = compose(z, next(x)) + y = extract(z, 3) + z = compose((mpz(10), -10 * y, mpz(0), mpz(1)), z) + yield y + + +def calc_ndigits(n): + return list(islice(gen_pi_digits(), n)) + + +def add_cmdline_args(cmd, args): + cmd.extend(("--digits", str(args.digits))) + + +if __name__ == "__main__": + runner = pyperf.Runner(add_cmdline_args=add_cmdline_args) + + cmd = runner.argparser + cmd.add_argument("--digits", type=int, default=DEFAULT_DIGITS, + help=("Number of computed pi digits " + f"(default: {DEFAULT_DIGITS})")) + + args = runner.parse_args() + runner.metadata["description"] = "Compute digits of pi." + runner.metadata["pidigits_ndigit"] = args.digits + runner.bench_func("pidigits", calc_ndigits, args.digits) diff --git a/docs/index.rst b/docs/index.rst index 4b44ea8f..ca09d65a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,3 +2,23 @@ Documentation for python-gmp ============================ .. automodule:: gmp + + +.. currentmodule:: gmp + +.. data:: mpz_info + + A named tuple that holds information about mpz type. The attributes are + read only. + + .. attribute:: mpz_info.bits_per_digit + + The size of a digit in bits. + + .. attribute:: sizeof_digit + + The size in bytes of the C type used to represent a digit. + + .. attribute:: bitcnt_max + + The maximal count of bits in an integer. diff --git a/gmp.c b/gmp.c index ad0402a3..691feb6a 100644 --- a/gmp.c +++ b/gmp.c @@ -405,9 +405,42 @@ new_impl(PyTypeObject *Py_UNUSED(type), PyObject *arg, PyObject *base_arg) } str: if (PyUnicode_Check(arg)) { - PyObject *res = (PyObject *)MPZ_from_str(arg, base); + if (PyUnicode_IS_ASCII(arg)) { + return (PyObject *)MPZ_from_str(arg, base); + } - return res; + PyObject *builtins = PyEval_GetFrameBuiltins(); + + if (!builtins) { + return NULL; /* LCOV_EXCL_LINE */ + } + + PyObject *int_func = PyDict_GetItemString(builtins, "int"); + + Py_DECREF(builtins); + if (!int_func) { + return NULL; /* LCOV_EXCL_LINE */ + } + + + PyObject *integer; + + if (Py_IsNone(base_arg)) { + integer = PyObject_CallOneArg(int_func, arg); + } + else { + integer = PyObject_CallFunction(int_func, "OO", arg, base_arg); + } + if (integer) { + PyObject *res = (PyObject *)MPZ_from_int(integer); + + if (!res) { + return NULL; /* LCOV_EXCL_LINE */ + } + Py_DECREF(integer); + return res; + } + return NULL; /* LCOV_EXCL_LINE */ } else if (PyByteArray_Check(arg) || PyBytes_Check(arg)) { const char *string; @@ -1603,8 +1636,8 @@ The signed argument indicates whether two’s complement is used."}, PyDoc_STRVAR(mpz_doc, "mpz(number=0, /)\nmpz(string, /, base=10)\n\n\ -Convert a number or an ASCII string to an integer. If numeric argument is not\n\ -an int subclass, return mpz(int(number)).\n\n\ +Convert a number or a string to an integer. If numeric argument is not\n\ +an int subclass, return same as mpz(int(number)).\n\n\ If argument is not a number or if base is given, then it must be a string,\n\ bytes, or bytearray instance representing an integer literal in the\n\ given base. The literal can be preceded by '+' or '-' and be surrounded\n\ @@ -2193,9 +2226,10 @@ A named tuple that holds information about mpz type.\n\ The attributes are read only."); static PyStructSequence_Field mpz_info_fields[] = { - {"bits_per_digit", "size of a digit in bits"}, - {"sizeof_digit", "size in bytes of the C type, used to represent a digit"}, - {"bitcnt_max", "maximal count of bits in integer"}, + {"bits_per_digit", "The size of a digit in bits."}, + {"sizeof_digit", + "The size in bytes of the C type used to represent a digit."}, + {"bitcnt_max", "The maximal count of bits in an integer."}, {NULL}}; static PyStructSequence_Desc mpz_info_desc = { @@ -2260,7 +2294,7 @@ gmp_exec(PyObject *m) "numbers.Integral.register(gmp.mpz)\n" "gmp.fac = gmp.factorial\n" "gmp.__all__ = ['comb', 'factorial', 'gcd', 'isqrt',\n" - " 'lcm', 'mpz', 'perm']\n" + " 'lcm', 'mpz', 'perm', 'mpz_info']\n" "gmp.__version__ = imp.version('python-gmp')\n"); PyObject *codeobj = Py_CompileString(str, "", Py_file_input); PyObject *res; @@ -2316,7 +2350,8 @@ static PyModuleDef_Slot gmp_slots[] = { static struct PyModuleDef gmp_module = { PyModuleDef_HEAD_INIT, .m_name = "gmp", - .m_doc = "Bindings to the GNU GMP for Python.", + .m_doc = "Bindings to the GNU GMP for Python, providing int-compatible\n\ +integer type and functions as in the math.integer submodule.", .m_size = 0, .m_methods = gmp_functions, .m_slots = gmp_slots, diff --git a/tests/test_mpz.py b/tests/test_mpz.py index 1f40f347..b429b0cd 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -198,12 +198,19 @@ def test_mpz_interface(): mpz(" ") with pytest.raises(ValueError, match="invalid literal"): mpz("qqq") + with pytest.raises(ValueError, match="invalid literal"): + mpz("ыыы") assert mpz() == mpz(0) == 0 assert mpz(" -123") == -123 assert mpz("123 ") == 123 assert mpz(" -123 ") == -123 assert mpz("+123") == 123 + assert mpz("١٢٣٤") == 1234 # unicode decimal digits + assert mpz("١٢٣٤", base=20) == 8864 + assert mpz("१23") == 123 assert mpz("\t123") == 123 + if platform.python_implementation() != "GraalVM": + assert mpz("\xa0123") == 123 # see oracle/graalpython#1209 assert mpz("-010") == -10 assert mpz("-10") == -10 assert mpz("0b_10", 0) == 2 diff --git a/utils.c b/utils.c index e5cae2ea..f9eebee6 100644 --- a/utils.c +++ b/utils.c @@ -61,3 +61,11 @@ gmp_parse_pyargs(const gmp_pyargs *fnargs, Py_ssize_t argidx[], } return 0; } + +#if PY_VERSION_HEX < 0x030D00A6 +PyObject * +PyEval_GetFrameBuiltins(void) +{ + return Py_XNewRef(PyEval_GetBuiltins()); +} +#endif diff --git a/utils.h b/utils.h index 78ee1a1d..afa75921 100644 --- a/utils.h +++ b/utils.h @@ -34,4 +34,8 @@ int gmp_parse_pyargs(const gmp_pyargs *fnargs, Py_ssize_t argidx[], PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames); +#if PY_VERSION_HEX < 0x030D00A6 +PyObject * PyEval_GetFrameBuiltins(void); +#endif + #endif /* UTILS_H */