Python declarative

Chris Angelico rosuav at gmail.com
Fri Jan 24 08:55:27 EST 2014


On Fri, Jan 24, 2014 at 11:49 PM, Frank Millman <frank at chagford.com> wrote:
>
> "Chris Angelico" <rosuav at gmail.com> wrote in message
> news:CAPTjJmo+EuY439wB0C8or+ZAcYeNR844HAKwL3i2+55dDE8LNA at mail.gmail.com...
>> On Fri, Jan 24, 2014 at 8:21 PM, Frank Millman <frank at chagford.com> wrote:
>>> I find that I am using JSON and XML more and more in my project, so I
>>> thought I would explain what I am doing to see if others think this is an
>>> acceptable approach or if I have taken a wrong turn.
>>
>> Please don't take this the wrong way, but the uses of JSON and XML
>> that you describe are still completely inappropriate. Python does not
>> need this sort of thing.
>>
>
> Chris, I really am very grateful for your detailed response. There is much
> food for thought there and I will have to read it a few times to glean as
> much as possible from it.

Thanks for taking it the right way :) If I thought you were doing a
terrible job, I'd just archive the thread and move on. The detailed
response means I think you're very close to the mark, but could
improve. :)

> Here are some initial thoughts.
>
> 1. I don't really understand how your system actually works! You mention
> Python code and helper functions, but I cannot picture the overall structure
> of the program. I don't know if it is possible to provide a siimple example,
> but it would certainly help.

Yeah, it was a bit vague. Unfortunately I haven't actually built any
complex UI in Python, ever, so I'll have to use C, C++, REXX, or Pike,
for the example. In C, it would be using the OS/2 APIs, not very
useful to anyone else. In C++, either Borland's ancient "Object
Windows Library", or the Win32 APIs - either way, not particularly
helpful. REXX you probably don't even know as a language, and you're
unlikely to know any of the GUI libraries I've used - GpfRexx, VREXX,
VX-REXX, VisPro REXX, DrRexx, or a handful of others. So the best bet
is Pike, where I've used GTK, which is also available for Python - but
there are distinct differences. (Oh, I've also used DBExpert, which is
a superb example of how frustrating it can be to try to create a
complex GUI in a "look how easy, it's just drag and drop, no code
needed" system. But that's more arcane stuff from the 1990s that you
most likely don't know, and definitely don't need to know.)

So, here's some code from Gypsum. Firstly, here's a simple window
layout (not strictly what I had in Gypsum):

mainwindow = GTK2.Window(0)->add(GTK2.Vbox(0,0)
    ->add(GTK2.Label("About Gypsum: big long multi-line string"))
    ->add(GTK2.HbuttonBox()
        ->add(GTK2.Button("Close"))
        ->add(GTK2.Button("Foobar"))
    )
);

The window has a simple structure. It contains a vertical box, which
contains a label and a horizontal button box; the latter contains two
buttons. (The real about dialog has only one button, making it a poor
example. I've no idea what clicking Foobar on an about dialog should
do.)

There's a nested structure to it, and it's all code. (It's actually
all one single expression. The add() methods return the parent object,
so they chain nicely.) This gives maximum flexibility, but it gets
repetitive at times, especially when I do up the character sheet.
Here's a screenshot of just the first page:

http://rosuav.com/charsheet.png

(That's how it appears on Windows, since I'm currently testing it for
support there. It also has to work - and look right - on Linux and Mac
OS.)

It's particularly complicated when laying out a table, because the
table is so powerful and flexible.

                GTK2.Table(6,2,0)

->attach(GTK2.Label((["label":"Keyword","xalign":1.0])),0,1,0,1,GTK2.Fill,GTK2.Fill,5,0)
                        ->attach_defaults(win->kwd=GTK2.Entry(),1,2,0,1)

->attach(GTK2.Label((["label":"Name","xalign":1.0])),0,1,1,2,GTK2.Fill,GTK2.Fill,5,0)
                        ->attach_defaults(win->name=GTK2.Entry(),1,2,1,2)
                        ->attach(GTK2.Label((["label":"Host
name","xalign":1.0])),0,1,2,3,GTK2.Fill,GTK2.Fill,5,0)
                        ->attach_defaults(win->hostname=GTK2.Entry(),1,2,2,3)

->attach(GTK2.Label((["label":"Port","xalign":1.0])),0,1,3,4,GTK2.Fill,GTK2.Fill,5,0)
                        ->attach_defaults(win->port=GTK2.Entry(),1,2,3,4)

->attach(GTK2.Label((["label":"Auto-log","xalign":1.0])),0,1,4,5,GTK2.Fill,GTK2.Fill,5,0)
                        ->attach_defaults(win->logfile=GTK2.Entry(),1,2,4,5)
                        ->attach(win->use_ka=GTK2.CheckButton("Use
keep-alive"),1,2,5,6,GTK2.Fill,GTK2.Fill,5,0) //No separate label

Horribly repetitive, and requires care to ensure that the
left/right/top/bottom boundaries are correct in all cases. Technically
it's possible for widgets to span rows or columns, and even to
overlap, but that's extremely rare compared to the common case of just
"lay these things out in a grid". (Though spanning columns was easy
enough to implement, so I did it.)

Here's the version with the first helper function:

                GTK2Table(({
                        ({"Keyword",win->kwd=GTK2.Entry()}),
                        ({"Name",win->name=GTK2.Entry()}),
                        ({"Host name",win->hostname=GTK2.Entry()}),
                        ({"Port",win->port=GTK2.Entry()}),
                        ({"Auto-log",win->logfile=GTK2.Entry()}),
                        ({"",win->use_ka=GTK2.CheckButton("Use keep-alive")}),
                }),(["xalign":1.0]))

I have to create a blank label in there to keep the positioning right
(the previous version simply didn't attach anything in that slot), but
that's a small price to pay. The readability has gone up hugely. And
yet, there's still a little more that can be done, as I realized when
I found myself duplicating the above structure a few times.

                two_column(({
                        "Keyword",win->kwd=GTK2.Entry(),
                        "Name",win->name=GTK2.Entry(),
                        "Host name",win->hostname=GTK2.Entry(),
                        "Port",win->port=GTK2.Entry(),
                        "Auto-log",win->logfile=GTK2.Entry(),
                        "",win->use_ka=GTK2.CheckButton("Use keep-alive"),
                }))

This is something that I could give to a non-programmer and expect him
to be able to edit, as it's about as clear as the equivalent JSON or
XML would be; there could be a few more tweaks done, perhaps, but it's
pretty readable like that. A non-programmer could, for instance, put
the "Auto-log" line above the "Host name", simply by reordering the
lines of code; it would do exactly what he expects. There's as much
fragility/rigidity in this as there is in any other structured system
(eg if you leave out a comma, this won't work - same with JSON or
XML).

In the specific case of the character sheet, I frequently create entry
fields (GTK2.Entry()) that are bound to specific data attributes. So
in any context where it's legal to write GTK2.Entry(), you can instead
write ef("some_name") and it'll make a properly bound entry field. And
if it's a numeric field, num("some_name") will shrink the default size
and center the text in it as well, which is what people expect of the
numeric fields. Shorthand for the common cases. And then there are
complex cases like the weapon stats block - it needs its keyword,
name, damage roll, enchantment bonus, etc, etc, all laid out nicely;
and there might be three of those weapon blocks. So that's broken out
into a function.

The thing is, if your XML window structure has enough power... well,
that's more part of the next two points:

> 2. I am not sure if you have created this to make it easy for *you* to
> create forms, or for others. And if the latter, must they be programmers?
> One of my goals is to allow non-programmers to modify forms and even create
> them from scratch. I did mention that one benefit of my approach is that I
> can design a gui that allows this, but I don't get the sense that your's
> does.
>
> 3. Thanks for the links to the Inner Platform Effect. I had not heard of it,
> but I recognise the symptoms, and I realise that I am skirting close to it
> in some areas.

If your XML window structure has enough power, it'll be nearly
impossible for a non-programmer to make anything but the most trivial
changes to it. (Changing a literal string is usually easy enough. Any
decent system will make _that_ possible, at least.) If a
non-programmer can truly create a form from scratch, that's a sign
that the forms are extremely simple and, if you'll excuse the pun,
formulaic; and if that's the case, it should be possible to make a
really easy-to-use helper function like my two_column() above.
Challenge: Without knowing anything about Pike, or the connect dialog,
add another check box called "Passive mode" called win->passive and
positioned just after the hostname and port. Like the other check box,
it doesn't need a separate label. Can you do it? Could someone who
knows enough to edit XML do it? I suspect probably they could. There's
some boilerplate to copy and paste (same in XML or code), there's some
readable text strings (names, labels, types), and there's punctuation
that makes decent sense.

> 4. You are right that some things cannot be totally abstracted, and can only
> be handled by code. My approach is as follows. If at least 80% of the
> structure can be handled without resorting to code, I think it is
> worthwhile, and I think I have more than achieved that. Of the balance, if
> at least 80% of requirements are 'standard', I can write the standard
> functions in Python, and create an XML tag that will cause the function to
> be invoked at run-time. The 'designer' still does not have to worry about
> Python.

That's nice in theory, but it means you have to plan everything out.
That also means you have to predict your names etc, and then deprecate
old names if you ever find that you made a mistake. When you start
with code, you have the flexibility of looking at actual existing
forms and deciding what should be broken out for simplicity.

> For the remainder, I have created an XML tag called 'pyfunc' that
> provides a path to a custom-written function. For this, a Python programmer
> will be required. So far I have only made use of this in my 'form designer',
> which is quite complex as it has to take the XML definition and 'explode' it
> into a number of in-memory tables so that it can be presented in gui form.
> However, the average user is not going to get their hands that dirty.

That's what I meant when I said a "drop to code" system. Depending on
how smooth the boundary is, that could be either horribly clunky or
reasonably clean; the clean one usually requires a lot of work. On the
flip side, if your GUI creation is *all* code, there's no boundary at
all, and no work.

My system starts by writing an absolute minimum of code and forcing
the form designer to do more work, and then progressively adds code
*that is actually useful* and makes the form design work easier.
Trying to do it all in XML means you have to first predict what will
be needed, and that inevitably means writing a whole lot of code that
isn't, as it turns out, useful. That's an up-front cost before you can
do anything, plus you can't get rid of any of it later without first
making sure no forms are using those tags. Ultimately, a full-featured
GUI is going to require that its form designer understand the
underlying system anyway (in my case that's GTK), because nothing will
change that; and if your forms designers have to understand how GTK
works, they probably have to understand how to write GTK-using code.
At very least, the basics of code (names, structure, etc) will be
critical, and that's true of any GUI builder. DBExpert required that
you think about all that sort of thing, and that's as close as I've
ever seen to a pure "no code needed, build a UI to view a database
table" system. So... instead of fighting that, embrace it! Let it
truly be code.

The structure (your XML interpreter) is code that has a maintenance
cost. The less of that you have, the less it takes to make changes.
And as a rule of thumb, less code means less bugs; and less
development prior to usage means less bugs, too. Let real-world usage
guide your code, and you'll spend less time building stuff you don't
need.

ChrisA



More information about the Python-list mailing list