classification
Title: Include column offsets for bytecode instructions
Type: Stage: patch review
Components: Versions:
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: BTaskaya, Dennis Sweeney, Mark.Shannon, ammar2, aroberge, brandtbucher, miss-islington, nedbat, pablogsal, serhiy.storchaka, terry.reedy
Priority: normal Keywords: patch

Created on 2021-04-27 10:58 by pablogsal, last changed 2021-07-25 22:01 by miss-islington.

Pull Requests
URL Status Linked Edit
PR 26955 merged pablogsal, 2021-06-29 15:06
PR 26958 merged ammar2, 2021-06-29 20:11
PR 26997 merged BTaskaya, 2021-07-02 17:34
PR 27011 merged BTaskaya, 2021-07-04 09:00
PR 27015 merged BTaskaya, 2021-07-04 15:13
PR 27023 merged ammar2, 2021-07-04 19:32
PR 27023 merged ammar2, 2021-07-04 19:32
PR 27023 merged ammar2, 2021-07-04 19:32
PR 27037 merged BTaskaya, 2021-07-05 17:04
PR 27047 merged ammar2, 2021-07-06 19:11
PR 27117 open BTaskaya, 2021-07-13 16:51
PR 27126 merged BTaskaya, 2021-07-13 20:44
PR 27217 merged BTaskaya, 2021-07-17 16:32
PR 27313 merged BTaskaya, 2021-07-23 19:08
PR 27336 merged BTaskaya, 2021-07-24 17:18
PR 27337 merged BTaskaya, 2021-07-24 17:20
PR 27339 merged BTaskaya, 2021-07-24 18:25
Messages (37)
msg392054 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-04-27 10:58
If we could include column offsets from the AST nodes for every bytecode instructions we could leverage these to offer much better tracebacks and a lot more information to debuggers and similar tools. For instance, in an expression such as:

z['aaaaaa']['bbbbbb']['cccccc']['dddddd].sddfsdf.sdfsdf

where one of these elements is None, we could tell exactly what of these pieces is the actual None and we could do some cool highlighting on tracebacks.

Similarly, coverage tools and debuggers could also make this distinction and therefore could offer reports with more granularity.

The cost is not 0: it would be two integers per bytecode instruction, but I think it may be worth the effort.
msg392062 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-04-27 12:33
I'm going to prepare a PEP since the discussion regarding if the two integers per bytecode are worth enough is going to be eternal.
msg392081 - (view) Author: Mark Shannon (Mark.Shannon) * (Python committer) Date: 2021-04-27 14:40
The additional cost will not only be the line number table, but we need to store the line for exceptions that are reraised after cleanup.
Adding a column will mean more stack consumption.
msg392082 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-04-27 14:44
Yup, but I still think is worth the cost, giving that debugging improvements are usually extremely popular among users.
msg392520 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-04-30 22:28
Specific examples of current messages and proposed improvements would help focus discussion.

If you are willing to only handle code lines up to 256 chars, only 2 bytes should be needed.  (0,0) or (255,255) could mean 'somewhere beyond the 256th char'.
msg392540 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-04-30 23:25
> Specific examples of current messages and proposed improvements would help focus discussion.


Yeah, I am proposing going from:

>>> x['aaaaaa']['bbbbbb']['cccccc']['dddddd'].sddfsdf.sdfsdf
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable

to

>>> x['aaaaaa']['bbbbbb']['cccccc']['dddddd'].sddfsdf.sdfsdf
                         ^^^^^^^^^^
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
msg392541 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-04-30 23:26
Basically, to highlight in all exceptions the range in the displayed line where the error ocurred. For instance:


>>> foo(a, b/z+2, c, 132432 /x, d /y)
                     ^^^^^^^^^
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
msg392555 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-05-01 00:51
Marking what expression evaluated to None would be extremely helpful.  So would marking the 0 denominator when there is more than one candidate: "e = a/b + c/d".  It should be easy to revise IDLE Shell's print_exception to tag the span.  In some cases, the code that goes from a traceback line to a line in a file and marks it could do the same.

What would you do when the expression is not the last line?

try:
    x/y
    ...
except Exception as e:
   ...
   raise e

The except and raise might even be in a separate module.
I look forward to the PEP and discussion.
msg392557 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-05-01 00:59
> So would marking the 0 denominator when there is more than one candidate: "e = a/b + c/d".

No, it will mark the offset of the bytecode that was getting executed when the exception was raised. Is just a way to mark what is raising the particular exception

> What would you do when the expression is not the last line?

There is some logic needed to re-raise exceptions. But basically it boils down to comparate the line numbers to decide if you want to propagate or not the offsets.
msg396866 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-02 14:10
New changeset 98eee94421dcb42c15f2d7fc4cd21357722fbe2a by Pablo Galindo in branch 'main':
bpo-43950: Add code.co_positions (PEP 657) (GH-26955)
https://github.com/python/cpython/commit/98eee94421dcb42c15f2d7fc4cd21357722fbe2a
msg396874 - (view) Author: miss-islington (miss-islington) Date: 2021-07-02 19:04
New changeset ec8759b060eff83ff466f42c5a96d83a685016ce by Batuhan Taskaya in branch 'main':
bpo-43950: optimize column table assembling with pre-sizing object (GH-26997)
https://github.com/python/cpython/commit/ec8759b060eff83ff466f42c5a96d83a685016ce
msg396950 - (view) Author: miss-islington (miss-islington) Date: 2021-07-04 18:02
New changeset 44f91fc802a2b71312e9abd9e8e9dcbf02e8216d by Batuhan Taskaya in branch 'main':
bpo-43950: use 0-indexed column offsets for bytecode positions (GH-27011)
https://github.com/python/cpython/commit/44f91fc802a2b71312e9abd9e8e9dcbf02e8216d
msg396957 - (view) Author: miss-islington (miss-islington) Date: 2021-07-04 19:05
New changeset 693cec0e2dcafa393ed5cdaa606f8dc8e3876adf by Batuhan Taskaya in branch 'main':
bpo-43950: include position in dis.Instruction (GH-27015)
https://github.com/python/cpython/commit/693cec0e2dcafa393ed5cdaa606f8dc8e3876adf
msg396962 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-04 23:14
New changeset 5644c7b3ffd49bed58dc095be6e6148e0bb4431e by Ammar Askar in branch 'main':
bpo-43950: Print columns in tracebacks (PEP 657) (GH-26958)
https://github.com/python/cpython/commit/5644c7b3ffd49bed58dc095be6e6148e0bb4431e
msg397109 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-07 19:07
New changeset 4823d9a51281ebbc8e8d82a0dd3edc7d13ea8ac7 by Ammar Askar in branch 'main':
bpo-43950: Add option to opt-out of PEP-657 (GH-27023)
https://github.com/python/cpython/commit/4823d9a51281ebbc8e8d82a0dd3edc7d13ea8ac7
msg397352 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-12 19:32
New changeset 1890dd235f618d60c938f6904d2e1a8a56f99c1c by Batuhan Taskaya in branch 'main':
bpo-43950: Specialize tracebacks for subscripts/binary ops (GH-27037)
https://github.com/python/cpython/commit/1890dd235f618d60c938f6904d2e1a8a56f99c1c
msg397368 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-13 00:29
New changeset 9c3eaf88dc5d5bed80cc45936de06b7b3162bc6d by Ammar Askar in branch 'main':
bpo-43950: Add documentation for PEP-657 (GH-27047)
https://github.com/python/cpython/commit/9c3eaf88dc5d5bed80cc45936de06b7b3162bc6d
msg397589 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-15 23:38
New changeset 919ad537510fdc2c750109e0bc4eceea234324b2 by Batuhan Taskaya in branch 'main':
bpo-43950: make BinOp specializations more reliable (GH-27126)
https://github.com/python/cpython/commit/919ad537510fdc2c750109e0bc4eceea234324b2
msg397666 - (view) Author: Ammar Askar (ammar2) * (Python committer) Date: 2021-07-16 20:51
As reported by Will McGugan on twitter:

Printing may be wrong for some unicode characters like:


Traceback (most recent call last):
  File "test.py", line 1, in <module>
    x = ("่ฏฅ" / 0) + (1 / 2)
         ~~~~~~^
TypeError: unsupported operand type(s) for /: 'str' and 'int'


and


Traceback (most recent call last):
  File "test.py", line 3, in <module>
    x = ("๐Ÿ’ฉ" / 0) + (1 / 2)
         ~~~~~~~
TypeError: unsupported operand type(s) for /: 'str' and 'int'
msg397667 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-16 20:54
Ah, this is our friend _PyPegen_byte_offset_to_character_offset. I am glad we refactor it to be used by the Parser and the traceback mechanism. Ammar, would you like to take a look?
msg397669 - (view) Author: Ammar Askar (ammar2) * (Python committer) Date: 2021-07-16 20:58
Aah, I won't have time to investigate until Monday. I'll take a look then unless someone gets to it first :)
msg397673 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-16 21:51
We can probably leverage the unicodedata_UCD_east_asian_width_impl function in the unicodedata extension module to get the width of every character as emojis normally take 2 characters in the terminal. We could expose the necessary functions from there and use them in the _PyPegen_byte_offset_to_character_offset function
msg397829 - (view) Author: Ammar Askar (ammar2) * (Python committer) Date: 2021-07-19 18:24
Had some time to look into this. Just to summarize this problem, it deals with unicode points that are single characters but take up more than the width of a single character, even with a monospace font [1].

In the examples from above, the Chinese character itself counts as one character in a Python string. However, notice that it needs two carets:

>>> x = "่ฏฅ"
>>> print(x)
่ฏฅ
>>> len(x)
1
>>> print(x + '\n' + '^^')
่ฏฅ
^^

This issue is somewhat font dependent, in the case of the emoji I know that windows sometimes renders emojis as single-character wide black-and-white glyphs or colorful ones depending on the program.

As Pablo alluded to, unicodedata.east_asian_width is probably the best solution we can implement. For these wide characters it provides:

>>> unicodedata.east_asian_width('๐Ÿ’ฉ')
'W'
>>> unicodedata.east_asian_width('่ฏฅ')
'W'

W corresponding to Wide. Whereas for regular width characters:

>>> unicodedata.east_asian_width('b')
'Na'
>>> unicodedata.east_asian_width('=')
'Na'

we get Neutral (Not East Asian). This can be used to count the "displayed width" of the characters and hence the carets. However, organization is going to be a bit tricky since we're currently using _PyPegen_byte_offset_to_character_offset to get offsets to use for string slicing in the ast segment parsing code. We might have to make a separate function that gets the font display-width.

-------------

[1] Way more details on this issue here: https://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html and an example of a Python library that tries to deal with this issue here: https://github.com/jquast/wcwidth
msg397837 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-07-19 21:03
The effort to match caret lines to general unicode is similar to a previous issue that was closed as futile.  (But I could not find it.)  It has a downside that should be considered.

The fundamental problem is that there is no fixed pitch font for unicode. (Let alone any font covering all of unicode.) Nor is there a single-double width definition, or font, for all of unicode.  Some character sets are not amenable to such treatment.

To see the problem easier, open, for instance, IDLE's option/settings dialog, showing the fonts tab and a multi-script sample.  On Windows, select what I believe is the most 'fixed' font -- Courier New.  ASCII, Latin1, IPA, Greek, Cyrillic, Hebrew, and Arabic are all rendered in the same fixed pitch.  But the last 4 Cyrillic characters of "...ะชัŠะญัั ัคัฌำœ" are extremely cramped and may be rendered differently from the rest.  The East Asian characters are in a different fixed pitch, about 1.6 times the Ascii, etc. fixed pitch.  (So the double-wide 2 is 1.6 rounded up.  And with some fonts, the East Asian scripts are not all the same pitch.)  The South Asian script are variable pitch and for the sample chars, average wider than 1 (they have 20 chars, like the Ascii, etc, lines).  Tamil, especially, has a wide range of widths, with the widest as wide as the East Asian chars.

On Windows, on my machine, pasting the sample text between quotes results in the Greek chars, the last 4 Cyrillic chars, and all Asian chars (including Hebrew and Arabic) being replaced by replacement chars.  (I thought that this was better once, but maybe I mis-remember.)  While one can get script-specific fonts, the fixed-pitch South Asian fonts I tried on Mac were hardly readable.  My conclusion is that people using certain scripts and anyone wanting a wide variety of scripts needs to use a GUI-based editor and shell rather than a fixed-pitch terminal/console.

As long as the caret line has 1 char per code char, a GUI program can use it to mark code characters, and do so differently for '~' and '^'.  If some of these chars are doubled, exact character information is lost.  If you go ahead with this, please use a third character, such as '-', for additions.  GUI programs could then ignore these, given that they can otherwise can get the start character information.
msg397844 - (view) Author: Ammar Askar (ammar2) * (Python committer) Date: 2021-07-19 22:47
I think this is the previous issue you are talking about Terry. https://bugs.python.org/issue24665

The correct algorithm is a little more complex than just using east_asian_widths. Ideally we would like to use the wcwidth function (https://man7.org/linux/man-pages/man3/wcwidth.3.html) which implements all the logic to somewhat properly figure out widths.

Given Terry's comments from above and the previous issues I'm starting to think the fix for this issue might not be worth the added complexity.
msg397882 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-20 15:42
New changeset fbc349ff790c21f1a59af939d42033470790c530 by Batuhan Taskaya in branch 'main':
bpo-43950: Distinguish errors happening on character offset decoding (GH-27217)
https://github.com/python/cpython/commit/fbc349ff790c21f1a59af939d42033470790c530
msg397909 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-20 21:06
I don't know, seems that rust can deal with this just fine:

โฏ cargo run
   Compiling rtest v0.1.0 (/home/pablogsal/rtest)
error[E0308]: mismatched types
 --> src/main.rs:7:10
  |
7 |     test("hellooooooo: ๐Ÿฅณ ๐Ÿ˜ ๐Ÿ˜’ ๐Ÿ˜ž ๐Ÿ˜” some emojis");
  |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |          |
  |          expected struct `String`, found `&str`
  |          help: try using a conversion method: `"hellooooooo: ๐Ÿฅณ ๐Ÿ˜ ๐Ÿ˜’ ๐Ÿ˜ž ๐Ÿ˜” some emojis".to_string()`
msg397910 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-20 21:07
In bpo this looks bad, but test this small program yourself and check the error in your terminal:


fn test(number: String) -> () {
    println!("Hello, world: {}", number);
}

fn main() {
    test("hellooooooo: ๐Ÿฅณ ๐Ÿ˜ ๐Ÿ˜’ ๐Ÿ˜ž ๐Ÿ˜” some emojis");
}
msg397911 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-20 21:09
They seem to be checking the unicode width as we are proposing here. Check the calls to unicode_width::UnicodeWidthChar::width:

https://github.com/rust-lang/rust/blob/master/compiler/rustc_errors/src/emitter.rs#L729-L733
msg397914 - (view) Author: Ammar Askar (ammar2) * (Python committer) Date: 2021-07-20 21:45
Indeed, and the unicode_width package seems to implement the wcwidth algorithm:

* https://github.com/unicode-rs/unicode-width/blob/master/src/tables.rs#L39-L48
* https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c

https://bugs.python.org/issue12568 is an issue from the past that proposed adding wcwidth to the standard library.
msg397915 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-20 21:51
Even if this doesn't cover all cases it seems that everyone else is going with this solution for what I can see, and currently this looks suboptimal on python because we get this wrong on the vast majority of cases so my suggestion is to try to fix this the same way rust on others are doing it even if there is some subset of cases where it won't fully work
msg398089 - (view) Author: Dennis Sweeney (Dennis Sweeney) * (Python triager) Date: 2021-07-23 18:58
bpo-44719 managed to make the `assert(source_line);` fail
msg398144 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2021-07-24 12:50
New changeset c8362314cce53a5b59da7523fbdfa00f122aa319 by Batuhan Taskaya in branch 'main':
bpo-43950: ensure source_line is present when specialising the traceback (GH-27313)
https://github.com/python/cpython/commit/c8362314cce53a5b59da7523fbdfa00f122aa319
msg398169 - (view) Author: Batuhan Taskaya (BTaskaya) * (Python committer) Date: 2021-07-24 17:49
New changeset ef8b8535cb3cd705392af9b927d6ff336d98427d by Batuhan Taskaya in branch 'main':
bpo-43950: check against the raw string, not the pyobject (GH-27337)
https://github.com/python/cpython/commit/ef8b8535cb3cd705392af9b927d6ff336d98427d
msg398170 - (view) Author: Batuhan Taskaya (BTaskaya) * (Python committer) Date: 2021-07-24 17:50
New changeset 4f5980a4f57dab68b9137304f58bd08891d43d5a by Batuhan Taskaya in branch 'main':
bpo-43950: support long lines in traceback.py (GH-27336)
https://github.com/python/cpython/commit/4f5980a4f57dab68b9137304f58bd08891d43d5a
msg398172 - (view) Author: Batuhan Taskaya (BTaskaya) * (Python committer) Date: 2021-07-24 18:28
There is a subtle difference between traceback.py and Python/traceback.c which makes the latter (C version) output the line without trimming the trailing whitespace. The Python version simply uses `.strip()` which strips both the left (starting) and right (ending) side. I'd personally go with adapting the C version to use full strip, since it makes more sense but I am open to comments on this. 

Here is an example test (needs GH 27339 first) (func( call has 2 trailing whitespace characters);
https://gist.github.com/isidentical/37ab1693d551b678a52626298d99e582
msg398197 - (view) Author: miss-islington (miss-islington) Date: 2021-07-25 22:01
New changeset 3e235e0447e373d81f195f4292959c7be9c013dc by Batuhan Taskaya in branch 'main':
bpo-43950: support some multi-line expressions for PEP 657 (GH-27339)
https://github.com/python/cpython/commit/3e235e0447e373d81f195f4292959c7be9c013dc
History
Date User Action Args
2021-07-25 22:01:51miss-islingtonsetmessages: + msg398197
2021-07-24 18:28:59BTaskayasetmessages: + msg398172
2021-07-24 18:25:25BTaskayasetpull_requests: + pull_request25882
2021-07-24 17:50:43BTaskayasetmessages: + msg398170
2021-07-24 17:49:35BTaskayasetmessages: + msg398169
2021-07-24 17:20:04BTaskayasetpull_requests: + pull_request25880
2021-07-24 17:18:08BTaskayasetpull_requests: + pull_request25879
2021-07-24 12:50:35pablogsalsetmessages: + msg398144
2021-07-23 19:08:48BTaskayasetpull_requests: + pull_request25858
2021-07-23 18:58:19Dennis Sweeneysetnosy: + Dennis Sweeney
messages: + msg398089
2021-07-20 21:51:09pablogsalsetmessages: + msg397915
2021-07-20 21:45:09ammar2setmessages: + msg397914
2021-07-20 21:09:52pablogsalsetmessages: + msg397911
2021-07-20 21:07:08pablogsalsetmessages: + msg397910
2021-07-20 21:06:28pablogsalsetmessages: + msg397909
2021-07-20 15:42:22pablogsalsetmessages: + msg397882
2021-07-19 22:47:07ammar2setmessages: + msg397844
2021-07-19 21:03:10terry.reedysetmessages: + msg397837
2021-07-19 18:24:00ammar2setmessages: + msg397829
2021-07-17 16:32:00BTaskayasetpull_requests: + pull_request25758
2021-07-16 21:51:22pablogsalsetmessages: + msg397673
2021-07-16 20:58:56ammar2setmessages: + msg397669
2021-07-16 20:54:39pablogsalsetmessages: + msg397667
2021-07-16 20:51:10ammar2setmessages: + msg397666
2021-07-15 23:38:19pablogsalsetmessages: + msg397589
2021-07-13 20:44:35BTaskayasetpull_requests: + pull_request25669
2021-07-13 16:51:02BTaskayasetpull_requests: + pull_request25664
2021-07-13 00:29:47pablogsalsetmessages: + msg397368
2021-07-12 19:32:48pablogsalsetmessages: + msg397352
2021-07-07 19:07:20pablogsalsetmessages: + msg397109
2021-07-06 19:11:22ammar2setpull_requests: + pull_request25603
2021-07-05 17:04:37BTaskayasetpull_requests: + pull_request25597
2021-07-04 23:14:40pablogsalsetmessages: + msg396962
2021-07-04 19:32:55ammar2setpull_requests: + pull_request25584
2021-07-04 19:32:54ammar2setpull_requests: + pull_request25583
2021-07-04 19:32:48ammar2setpull_requests: + pull_request25582
2021-07-04 19:05:12miss-islingtonsetmessages: + msg396957
2021-07-04 18:02:31miss-islingtonsetmessages: + msg396950
2021-07-04 15:13:06BTaskayasetpull_requests: + pull_request25574
2021-07-04 09:00:50BTaskayasetpull_requests: + pull_request25571
2021-07-02 19:04:34miss-islingtonsetnosy: + miss-islington
messages: + msg396874
2021-07-02 17:34:38BTaskayasetpull_requests: + pull_request25557
2021-07-02 14:10:15pablogsalsetmessages: + msg396866
2021-06-29 20:11:11ammar2setnosy: + ammar2
pull_requests: + pull_request25524
2021-06-29 16:54:53arobergesetnosy: + aroberge
2021-06-29 15:06:20pablogsalsetkeywords: + patch
stage: patch review
pull_requests: + pull_request25521
2021-05-05 14:13:53BTaskayasetnosy: + BTaskaya
2021-05-01 00:59:23pablogsalsetmessages: + msg392557
2021-05-01 00:51:48terry.reedysetmessages: + msg392555
2021-04-30 23:51:16brandtbuchersetnosy: + brandtbucher
2021-04-30 23:26:53pablogsalsetmessages: + msg392541
2021-04-30 23:25:22pablogsalsetmessages: + msg392540
2021-04-30 22:28:13terry.reedysetnosy: + terry.reedy
messages: + msg392520
2021-04-27 14:44:12pablogsalsetmessages: + msg392082
2021-04-27 14:40:46Mark.Shannonsetmessages: + msg392081
2021-04-27 12:33:31pablogsalsetmessages: + msg392062
2021-04-27 10:58:54pablogsalsetnosy: + nedbat, serhiy.storchaka
2021-04-27 10:58:38pablogsalcreate