File np.patch of Package python-pythran

From f78b07d7648a1efe543c01cc4019928791eb39e9 Mon Sep 17 00:00:00 2001
From: serge-sans-paille <serge.guelton@telecom-bretagne.eu>
Date: Sun, 8 Jun 2025 19:00:10 +0200
Subject: [PATCH 1/4] Use more generic way of checking bool value of an object

This avoid bad interaction with numpy.bool_ as reported in #2322
---
 pythran/pythonic/types/bool.hpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pythran/pythonic/types/bool.hpp b/pythran/pythonic/types/bool.hpp
index 35ac01d37..1436871f1 100644
--- a/pythran/pythonic/types/bool.hpp
+++ b/pythran/pythonic/types/bool.hpp
@@ -28,7 +28,7 @@ inline bool from_python<bool>::convert(PyObject *obj)
   else if (obj == Py_False)
     return false;
   else
-    return PyInt_AsLong(obj);
+    return PyObject_IsTrue(obj);
 }
 
 PYTHONIC_NS_END

From 2847f56dbbb388aeab5f3cab18c70fe26298d254 Mon Sep 17 00:00:00 2001
From: serge-sans-paille <serge.guelton@telecom-bretagne.eu>
Date: Tue, 10 Jun 2025 07:23:03 +0200
Subject: [PATCH 2/4] Add support for numpy.frombuffer

Even though we have very partial support for bytes in Pythran
---
 pythran/analyses/dependencies.py              |  2 ++
 pythran/backend.py                            |  8 +++++
 pythran/pythonic/include/numpy/frombuffer.hpp | 26 ++++++++++++++
 pythran/pythonic/numpy/frombuffer.hpp         | 35 +++++++++++++++++++
 pythran/tables.py                             |  1 +
 pythran/tests/test_numpy_func0.py             | 14 +++++++-
 pythran/tests/test_numpy_random.py            |  4 +--
 pythran/types/conversion.py                   |  1 +
 8 files changed, 88 insertions(+), 3 deletions(-)
 create mode 100644 pythran/pythonic/include/numpy/frombuffer.hpp
 create mode 100644 pythran/pythonic/numpy/frombuffer.hpp

diff --git a/pythran/analyses/dependencies.py b/pythran/analyses/dependencies.py
index f153558f8..e0988e103 100644
--- a/pythran/analyses/dependencies.py
+++ b/pythran/analyses/dependencies.py
@@ -127,6 +127,8 @@ def visit_Yield(self, node):
     def visit_Constant(self, node):
         if node.value is None:
             self.result.add(('builtins', 'None'))
+        elif isinstance(node.value, bytes):
+            self.result.add(('types', 'str'))  # FIXME: using str as backend
         elif isinstance(node.value, str):
             self.result.add(('types', 'str'))
         elif isinstance(node.value, complex):
diff --git a/pythran/backend.py b/pythran/backend.py
index e5728ab3e..7b12773fa 100644
--- a/pythran/backend.py
+++ b/pythran/backend.py
@@ -1012,6 +1012,14 @@ def visit_Constant(self, node):
             ret = 'pythonic::builtins::None'
         elif isinstance(node.value, bool):
             ret = str(node.value).lower()
+        elif isinstance(node.value, bytes):
+            quoted = "".join('\\' + hex(b)[1:] for b in node.value)
+            # FIXME: using str type as backend
+            if len(node.value) == 1:
+                quoted = quoted.replace("'", r"\'")
+                ret = 'pythonic::types::chr(\'' + quoted + '\')'
+            else:
+                ret = 'pythonic::types::str("' + quoted + '")'
         elif isinstance(node.value, str):
             quoted = quote_cxxstring(node.value)
             if len(node.value) == 1:
diff --git a/pythran/pythonic/include/numpy/frombuffer.hpp b/pythran/pythonic/include/numpy/frombuffer.hpp
new file mode 100644
index 000000000..bb685d8b5
--- /dev/null
+++ b/pythran/pythonic/include/numpy/frombuffer.hpp
@@ -0,0 +1,26 @@
+#ifndef PYTHONIC_INCLUDE_NUMPY_FROMBUFFER_HPP
+#define PYTHONIC_INCLUDE_NUMPY_FROMBUFFER_HPP
+
+#include "pythonic/include/numpy/float64.hpp"
+#include "pythonic/include/types/list.hpp"
+#include "pythonic/include/types/ndarray.hpp"
+#include "pythonic/include/types/str.hpp"
+#include "pythonic/include/utils/functor.hpp"
+
+#include <limits>
+#include <sstream>
+
+PYTHONIC_NS_BEGIN
+
+namespace numpy
+{
+  template <class dtype = functor::float64>
+  types::ndarray<typename dtype::type, types::pshape<long>>
+  frombuffer(types::str const &string, dtype d = dtype(), long count = -1,
+             long offset = 0);
+
+  DEFINE_FUNCTOR(pythonic::numpy, frombuffer);
+} // namespace numpy
+PYTHONIC_NS_END
+
+#endif
diff --git a/pythran/pythonic/numpy/frombuffer.hpp b/pythran/pythonic/numpy/frombuffer.hpp
new file mode 100644
index 000000000..c0f625142
--- /dev/null
+++ b/pythran/pythonic/numpy/frombuffer.hpp
@@ -0,0 +1,35 @@
+#ifndef PYTHONIC_NUMPY_FROMBUFFER_HPP
+#define PYTHONIC_NUMPY_FROMBUFFER_HPP
+
+#include "pythonic/include/numpy/frombuffer.hpp"
+
+#include "pythonic/types/list.hpp"
+#include "pythonic/types/ndarray.hpp"
+#include "pythonic/types/str.hpp"
+#include "pythonic/utils/functor.hpp"
+
+#include <limits>
+#include <sstream>
+
+PYTHONIC_NS_BEGIN
+
+namespace numpy
+{
+  template <class dtype>
+  types::ndarray<typename dtype::type, types::pshape<long>>
+  frombuffer(types::str const &string, dtype d, long count, long offset)
+  {
+    if (count < 0)
+      count = string.size() / sizeof(typename dtype::type);
+    types::pshape<long> shape = count;
+    utils::shared_ref<types::raw_array<typename dtype::type>> buffer(
+        std::get<0>(shape));
+    auto const *tstring =
+        reinterpret_cast<typename dtype::type const *>(string.c_str()) + offset;
+    std::copy(tstring, tstring + std::get<0>(shape), buffer->data);
+    return {buffer, shape};
+  }
+} // namespace numpy
+PYTHONIC_NS_END
+
+#endif
diff --git a/pythran/tables.py b/pythran/tables.py
index a61709d19..bf0d9b220 100644
--- a/pythran/tables.py
+++ b/pythran/tables.py
@@ -3752,6 +3752,7 @@ def expand_numpy_2_args(args, defaults=None, force=False):
         "fmin": UFunc(REDUCED_BINARY_UFUNC),
         "fmod": UFunc(BINARY_UFUNC),
         "frexp": ConstFunctionIntr(),
+        "frombuffer": ConstFunctionIntr(),
         "fromfunction": ConstFunctionIntr(),
         "fromiter": ConstFunctionIntr(args=("iterable", "dtype", "count"),
                                       defaults=(-1,)),
diff --git a/pythran/tests/test_numpy_func0.py b/pythran/tests/test_numpy_func0.py
index 41f716d90..a97a43c50 100644
--- a/pythran/tests/test_numpy_func0.py
+++ b/pythran/tests/test_numpy_func0.py
@@ -560,11 +560,23 @@ def test_fromstring1(self):
         self.run_test("def np_fromstring1(a): from numpy import fromstring, uint8 ; a = '\x01\x02\x03\x04' ; return fromstring(a, uint8,3)", '\x01\x02\x03\x04', np_fromstring1=[str])
 
     def test_fromstring2(self):
-        self.run_test("def np_fromstring2(a): from numpy import fromstring, uint32 ; return fromstring(a, uint32,-1, ' ')", '1 2 3 4', np_fromstring2=[str])
+        self.run_test("def np_fromstring2(a): from numpy import fromstring, uint32 ; return fromstring(a, uint32,-1, ' ')", '1 20 3 40', np_fromstring2=[str])
 
     def test_fromstring3(self):
         self.run_test("def np_fromstring3(a): from numpy import fromstring, uint32 ; return fromstring(a, uint32,2, ',')", '1,2, 3, 4', np_fromstring3=[str])
 
+    def test_frombuffer0(self):
+        self.run_test("def np_frombuffer0(a): from numpy import frombuffer, uint8 ; return frombuffer(b'\x01\x02' * a, uint8)", 1, np_frombuffer0=[int])
+
+    def test_frombuffer1(self):
+        self.run_test("def np_frombuffer1(a): from numpy import frombuffer, uint8 ; return frombuffer(b'\x01\x02\x03\x04' * a, uint8, 3)", 1, np_frombuffer1=[int])
+
+    def test_frombuffer2(self):
+        self.run_test("def np_frombuffer2(a): from numpy import frombuffer, uint16 ; return frombuffer(a * b'\x01\x02', uint16)", 1, np_frombuffer2=[int])
+
+    def test_frombuffer3(self):
+        self.run_test("def np_frombuffer3(a): from numpy import frombuffer, int8 ; return frombuffer(a * b'\x01\x02\x03\x04', int8, 3, 1)", 1, np_frombuffer3=[int])
+
     def test_outer0(self):
         self.run_test("def np_outer0(x): from numpy import outer ; return outer(x, x+2)", numpy.arange(6).reshape(2,3), np_outer0=[NDArray[int,:,:]])
 
diff --git a/pythran/tests/test_numpy_random.py b/pythran/tests/test_numpy_random.py
index aa3f22815..6547a2e92 100644
--- a/pythran/tests/test_numpy_random.py
+++ b/pythran/tests/test_numpy_random.py
@@ -638,9 +638,9 @@ def test_numpy_random_bytes1(self):
         self.run_test("""
             def numpy_random_bytes1(n):
                 from numpy.random import bytes
-                from numpy import mean, fromstring, uint8, asarray
+                from numpy import mean, frombuffer, uint8, asarray
                 a = bytes(n)
-                return (abs(mean(asarray(fromstring(a, uint8), dtype=float)) - 127.5) < .05)""",
+                return (abs(mean(asarray(frombuffer(a, uint8), dtype=float)) - 127.5) < .05)""",
                       10 ** 8, numpy_random_bytes1=[int])
 
     ###########################################################################
diff --git a/pythran/types/conversion.py b/pythran/types/conversion.py
index e149b7fb4..b3c86b5fd 100644
--- a/pythran/types/conversion.py
+++ b/pythran/types/conversion.py
@@ -9,6 +9,7 @@
 PYTYPE_TO_CTYPE_TABLE = {
     numpy.uint: 'npy_uint',
     #
+    bytes: 'pythonic::types::str',  # FIXME: using types::str as backend
     complex: 'std::complex<double>',
     bool: 'bool',
     int: 'long',

From c01d4224ca0c34af6157ee4e82893a82f3e9bb43 Mon Sep 17 00:00:00 2001
From: serge-sans-paille <serge.guelton@telecom-bretagne.eu>
Date: Sun, 8 Jun 2025 19:15:28 +0200
Subject: [PATCH 3/4] Do not test binary mode of numpy.fromstring for recent
 numpy version

It's no longer supported as of numpy 2.3, see
https://numpy.org/devdocs/release/2.3.0-notes.html and
https://github.com/numpy/numpy/pull/28254

Fix #2322
---
 pythran/tests/test_numpy_func0.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pythran/tests/test_numpy_func0.py b/pythran/tests/test_numpy_func0.py
index a97a43c50..91c37e979 100644
--- a/pythran/tests/test_numpy_func0.py
+++ b/pythran/tests/test_numpy_func0.py
@@ -553,9 +553,11 @@ def test_fromfile5(self):
         finally:
             os.remove(temp_name)
 
+    @unittest.skipIf(np_version > version.Version("2.2"), reason="np.fromstring no longer supports binary mode")
     def test_fromstring0(self):
         self.run_test("def np_fromstring0(a): from numpy import fromstring, uint8 ; return fromstring(a, uint8)", '\x01\x02', np_fromstring0=[str])
 
+    @unittest.skipIf(np_version > version.Version("2.2"), reason="np.fromstring no longer supports binary mode")
     def test_fromstring1(self):
         self.run_test("def np_fromstring1(a): from numpy import fromstring, uint8 ; a = '\x01\x02\x03\x04' ; return fromstring(a, uint8,3)", '\x01\x02\x03\x04', np_fromstring1=[str])
 

From ef869953fbb16a31c5eb06c72296d4f23cf73e55 Mon Sep 17 00:00:00 2001
From: serge-sans-paille <serge.guelton@telecom-bretagne.eu>
Date: Sun, 8 Jun 2025 19:22:49 +0200
Subject: [PATCH 4/4] Adjust doc validation to recent python 3.13

- force COLUMNS width for reproducible argparse help output
- gast Constant pretty printing has changed

Fix #2317
---
 .github/workflows/core.yml |  2 +-
 docs/TUTORIAL.rst          |  6 +++---
 pythran/tests/test_xdoc.py | 14 +++++++++++++-
 3 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index 5dca25973..8609be87e 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-22.04
     strategy:
       matrix:
-        python-version: [3.7, 3.9, 3.11, 3.12, pypy-3.9]
+        python-version: [3.7, 3.9, 3.11, 3.12, 3.13, pypy-3.9]
         cpp-version: [g++-12, clang-13]
     steps:
     - uses: actions/checkout@v2
diff --git a/docs/TUTORIAL.rst b/docs/TUTORIAL.rst
index e1dd80c4f..31ff4fffc 100644
--- a/docs/TUTORIAL.rst
+++ b/docs/TUTORIAL.rst
@@ -20,7 +20,7 @@ Python ships a standard module, ``ast`` to turn Python code into an AST. For ins
   >>> code = "a=1"
   >>> tree = ast.parse(code)  # turn the code into an AST
   >>> print(ast.dump(tree))  # view it as a string
-  Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=1, kind=None))])
+  Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=1))])
 
 Deciphering the above line, one learns that the single assignment is parsed as
 a module containing a single statement, which is an assignment to a single
@@ -33,7 +33,7 @@ Eventually, one needs to parse more complex codes, and things get a bit more cry
   ...     return n if n< 2 else fib(n-1) + fib(n-2)"""
   >>> tree = ast.parse(fib_src)
   >>> print(ast.dump(tree))
-  Module(body=[FunctionDef(name='fib', args=arguments(args=[Name(id='n', ctx=Param())]), body=[Return(value=IfExp(test=Compare(left=Name(id='n', ctx=Load()), ops=[Lt()], comparators=[Constant(value=2, kind=None)]), body=Name(id='n', ctx=Load()), orelse=BinOp(left=Call(func=Name(id='fib', ctx=Load()), args=[BinOp(left=Name(id='n', ctx=Load()), op=Sub(), right=Constant(value=1, kind=None))]), op=Add(), right=Call(func=Name(id='fib', ctx=Load()), args=[BinOp(left=Name(id='n', ctx=Load()), op=Sub(), right=Constant(value=2, kind=None))]))))])])
+  Module(body=[FunctionDef(name='fib', args=arguments(args=[Name(id='n', ctx=Param())]), body=[Return(value=IfExp(test=Compare(left=Name(id='n', ctx=Load()), ops=[Lt()], comparators=[Constant(value=2)]), body=Name(id='n', ctx=Load()), orelse=BinOp(left=Call(func=Name(id='fib', ctx=Load()), args=[BinOp(left=Name(id='n', ctx=Load()), op=Sub(), right=Constant(value=1))]), op=Add(), right=Call(func=Name(id='fib', ctx=Load()), args=[BinOp(left=Name(id='n', ctx=Load()), op=Sub(), right=Constant(value=2))]))))])])
 
 The idea remains the same. The whole Python syntax is described in
 http://docs.python.org/2/library/ast.html and is worth a glance, otherwise
@@ -199,7 +199,7 @@ constant expressions. In the previous code, there is only two constant
 
   >>> ce = pm.gather(analyses.ConstantExpressions, tree)
   >>> sorted(map(ast.dump, ce))
-  ["Attribute(value=Name(id='math', ctx=Load()), attr='cos', ctx=Load())", 'Constant(value=3, kind=None)']
+  ["Attribute(value=Name(id='math', ctx=Load()), attr='cos', ctx=Load())", 'Constant(value=3)']
 
 One of the most critical analyse of Pythran is the points-to analysis. There
 are two flavors of this analyse, one that computes an over-set of the aliased
diff --git a/pythran/tests/test_xdoc.py b/pythran/tests/test_xdoc.py
index 862dec1fa..1dddf36ca 100644
--- a/pythran/tests/test_xdoc.py
+++ b/pythran/tests/test_xdoc.py
@@ -20,6 +20,8 @@ class TestDoctest(unittest.TestCase):
 
     @pytest.mark.skipif(sys.platform == "win32",
                         reason="We should create a file for windows.")
+    @pytest.mark.skipif(sys.version_info < (3, 13),
+                        reason="ast output changed with 3.13")
     def test_tutorial(self):
         failed, _ = doctest.testfile('../../docs/TUTORIAL.rst')
         self.assertEqual(failed, 0)
@@ -34,9 +36,19 @@ def test_internal(self):
 
     @pytest.mark.skipif(sys.platform == "win32",
                         reason="We should create a file for windows.")
+    @pytest.mark.skipif(sys.version_info <= (3, 12),
+                        reason="argparse output changed with 3.13")
     def test_cli(self):
         tmpfile = self.adapt_rst('../../docs/CLI.rst')
-        failed, _ = doctest.testfile(tmpfile, False)
+        columns = os.environ.get('COLUMNS', None)
+        os.environ['COLUMNS'] = '80'
+        try:
+            failed, _ = doctest.testfile(tmpfile, False)
+        finally:
+            if columns is None:
+                del os.environ['COLUMNS']
+            else:
+                os.environ['COLUMNS'] = columns
         self.assertEqual(failed, 0)
         os.remove(tmpfile)
 
openSUSE Build Service is sponsored by