Skip to content

Questions about pep3118 format strings #24428

Open
@hoodmane

Description

@hoodmane

I am trying to understand pep3118 since it is essentially undocumented, see the discussion here: https://discuss.python.org/t/question-pep-3118-format-strings-and-the-buffer-protocol/31264/7

@mattip @seberg @rgommers @pitrou

Numpy implements a large subset of it in numpy/core/_internal.py. I think the parser in _internal.py implements the following lark grammar:

Lark grammar for numpy's _dtype_from_pep3118
?start: root
root: entry+
?entry: ( array | padding | _normal_entry ) name?

struct: "T{" entry* "}"
padding: "x"
name:  ":" IDENTIFIER ":"

array: shape _normal_entry
shape: "(" _shape_body ")"
_shape_body: (NUMBER ",")* NUMBER

_normal_entry: byteorder? repeat? ( struct | prim )
byteorder: BYTEORDER
repeat: NUMBER
prim: PRIMITIVE

IDENTIFIER: /[^:^\s]+/
NUMBER: ("0".."9")+
BYTEORDER: "@" | "=" | "<" | ">" | "^" | "!"
PRIMITIVE: "Zf" | "Zd" | "Zg" | /[?cbBhHiIlLqQfdgswOx]/

%ignore /\s+/

There are a few things I think are weird about this grammar:

The location of the byte order marks in relation to shapes

The pep says:

Endian-specification (‘!’, ‘@’,’=’,’>’,’<’, ‘^’) is also allowed inside the string so that it can change if needed. The previously-specified endian string is in force until changed. The default endian is ‘@’ which means native data-types and alignment. If un-aligned, native data-types are requested, then the endian specification is ‘^’.

This is completely ambiguous about where these marks can go. Prior to pep3118 it seems that the marks are only allowed at the very start of the format string. It seems to me that the most logical location would be that one is allowed between each pair of adjacent entries. But _dtype_from_pep3118 expects them to come between the shape and the primitive:

>>> _dtype_from_pep3118("@(3,1)i") # @ before shape not allowed
ValueError: Unknown PEP 3118 data type specifier '(3,1)i'
>>> _dtype_from_pep3118("(3,1)@i") # @ between shape and i allowed
dtype(('<i4', (3, 1)))

This would sort of make sense if the mark only affected the current entry but it also affects all following ones, making the location a bit perplexing. This becomes particularly noticeable when you look at the parse trees: since it affects all following entries, it should come next to the entries but the parser grammar above makes the order mark a child of a particular entry.

I think this is a bug which should be fixed by the following patch:

patch
--- a/numpy/core/_internal.py
+++ b/numpy/core/_internal.py
@@ -673,12 +673,6 @@ def __dtype_from_pep3118(stream, is_subdtype):
         if stream.consume('}'):
             break
 
-        # Sub-arrays (1)
-        shape = None
-        if stream.consume('('):
-            shape = stream.consume_until(')')
-            shape = tuple(map(int, shape.split(',')))
-
         # Byte order
         if stream.next in ('@', '=', '<', '>', '^', '!'):
             byteorder = stream.advance(1)
@@ -686,6 +680,12 @@ def __dtype_from_pep3118(stream, is_subdtype):
                 byteorder = '>'
             stream.byteorder = byteorder
 
+        # Sub-arrays (1)
+        shape = None
+        if stream.consume('('):
+            shape = stream.consume_until(')')
+            shape = tuple(map(int, shape.split(',')))
+
         # Byte order characters also control native vs. standard type sizes
         if stream.byteorder in ('@', '^'):
             type_map = _pep3118_native_map

(4)h vs 4h vs hhhh

In the struct module documentation it says:

the format string '4h' means exactly the same as 'hhhh'.

But _dtype_from_pep3118 disagrees: it gives the same output for 4h and (4)h but both are different from the output for hhhh. Then there is the issue of (4)4h, which is treated as a array of 4 arrays of 4 h's, so not the same as (4,4)h. Also, perplexingly (4)(4)h is a syntax error. I think (4)4h should be the same as (4)T{hhhh}.

Also as I said, it seems to me that it makes more sense to allow arbitrary nested arrays like (4)(4)h to mean the current thing that (4)4h means.

Arrays of padding

I think it's weird that _dtype_from_pep3118 accepts arrays of padding like (4, 4)x. Isn't this properly rendered as 16x? It gives the same output. My grammar doesn't allow it.

Named padding

Is it intended that can be named? If you need a name for it, is it padding anymore?

A lark grammar with my suggested modifications:

Details
?start: root
root: _entry+
_entry: byteorder? entry
?entry:  padding | (_normal_entry  name?)

padding: NUMBER? "x"
name:  ":" IDENTIFIER ":"

_normal_entry: array | (repeat? ( struct | prim ))

struct: "T{" _entry* "}"

array: shape _normal_entry
shape: "(" _shape_body ")"
_shape_body: (NUMBER ",")* NUMBER


byteorder: BYTEORDER
repeat: NUMBER
prim: PRIMITIVE


IDENTIFIER: /[^:^\s]+/
NUMBER: ("0".."9")+
BYTEORDER: "@" | "=" | "<" | ">" | "^" | "!"
PRIMITIVE: "Zf" | "Zd" | "Zg" | /[?cbBhHiIlLqQfdgswOx]/

%ignore /\s+/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions