Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/backport.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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-
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
Expand All @@ -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
Expand Down
70 changes: 70 additions & 0 deletions bench/pidigits.py
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 20 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
53 changes: 44 additions & 9 deletions gmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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\
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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, "<file>", Py_file_input);
PyObject *res;
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions tests/test_mpz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Loading