Help me pick an API design (OO vs functional)

M

Michael Herrmann

Even if it does, it'll be polluted with every other global. Methods
don't have that problem. On the flip side, since presumably this is
(will be) a module, anyone who wants autocomplete of its top-level
functions can simply "import module" instead of "from module import
*", which will do the same namespacing.

True! I don't think "polluting" the global namespace is that much of an issue.
 
M

Michael Herrmann

----- Original Message -----

^
|
here, this is an above example :D

Ah, so you meant "is also easy to read" ;) I agree but the example with global functions is even easier to read. I guess I am being pretty anal about these issues, but I see every unnecessary syntax we can save as a win.
[snip]
Doesn't the IPython do auto-completion for "global" functions?

Yes it does, but as Chris pointed out, your global/module namespace will be "polluted" by a lot of names.
By using completion on an object, you get the method it has access to, which is very useful to narrow down what you can do with it.

I see. I know you prefer design #1 but that would at least place design #4 over #3, right?

Thanks.
Michael
www.getautoma.com
 
D

Dave Angel

It's an interesting idea. But why not give this write(...) to them in the first place?

Just to be clear, I was avoiding the problem of having two ways of
accessing each function, since most of us prefer the methods, and you
have some users who prefer simple functions.
Am I the only one who appreciates the simplicity of

start("Notepad")
write("Hello World!")
press(CTRL + 's')
write("test.txt", into="File name")
click("Save")
press(ALT + F4)

over

notepad = start("Notepad")
notepad.write("Hello World!")
notepad.press(CTRL + 's')
notepad.write("test.txt", into="File name")
notepad.click("Save")
notepad.press(ALT + F4)?


save() is not a function, but I assume you mean the action that opens the "Save" dialogue (I think that'd be `press(CTRL + 's')`). You are right that it's nice for it to be explicit. However, in 95% of cases, the window you want the next action to be performed in is the window that is currently active. I appreciate the explicitness, but to force it on the user for only 5% of cases seems a bit much.


No. Internally, we remember which window is the currently active window. If you just run a script without user-intervention, this will be the respective foreground window. If some other window is in the foreground - which most typically happens when the user is interactively entering commands one after the other, so the foreground window is the console window, we do switch to the window that's supposed to be the active one. It may sound like black magic, but it works very well in practice, and really is not too ambiguous. When you read a script like

start("Notepad")
write("Hello World")
press(CTRL + 's')
write("test.txt", into="File name")
click("Save")
click("Close")

I hold that you intuitively know what's going on, without even thinking about window switching.

Until the program you're scripting makes some minor change in its
interface, or has something conditional on an attribute not intuitively
obvious.

Also, it seems that in this thread, we are using "window" both to refer
to a particular application instance (like Notepad1 and Notepad2), and
to refer to windows within a single application.

Anyway, if you're only automating a few specific apps, you're not likely
to run into the problems that methods were intended to address. After
all, Notepad's bugs haven't seemed to change for a couple of decades, so
why should they fix anything now?

Having a selected window be an implied object for those global functions
yields at least the same problems as any writable global.
Multithreading, unexpected side effects from certain functions,
callbacks, etc.

As long as you know the program is going to be simple, pile on the
globals. But as soon as it advances, each of them is a trap to fall into.
 
S

Steven D'Aprano

It's an interesting idea. But why not give this write(...) to them in
the first place? Am I the only one who appreciates the simplicity of

start("Notepad")
write("Hello World!")
press(CTRL + 's')
write("test.txt", into="File name")
click("Save")
press(ALT + F4)

over

notepad = start("Notepad")
notepad.write("Hello World!")
notepad.press(CTRL + 's')
notepad.write("test.txt", into="File name")
notepad.click("Save")
notepad.press(ALT + F4)?

You are not the only one.

I suggest that you have a set of functions that work on "the current
window", whatever that is. Preferably there should always be a current
window, but if not, ensure that you give a clear error message.

Then you have syntax for operating on any named(?) window. So a user can
implicitly operate on the current window:

select(notepad)
write("goodbye cruel world")
save()

or explicitly on any window they like:

excel.quit()


I suggest you dig up an old book on "Hypercard", for Apple Macs in the
1980s and 90s. Back in the day, Macs could only run a single application
at a time, and Hypercard was limited to a single window at a time (called
a "stack"). But that stack (think: window) could have multiple
"cards" (think: window tabs), one of which was always current.
Hypercard's built-in programming language Hypertalk let you do things
like this:


go to stack "Notepad"
type "goodbye cruel world" in field "main" of card 7
click button "Save"

click button "Quit" of card "Main" of stack "Excel"


(more or less... it's been a few years since I've had a classic Mac
capable of running Hypercard.)
 
M

Mitya Sirenef

Thank you for your reply. What do you think of Chris Angelico's points?


At the __exit__, further commands are no longer routed to that window;
if it was a nested context, window is switched to the outer context,
WHEN there are commands in it (i.e. on the first command). This seems
pretty intuitive to me:

with notepad1:
^S
with notepad2:
^S
write('something')
What I am most afraid of: that the window that's currently the context "disappears":
notepad = start("Notepad")
with notepad:
press(ALT + TAB)
write("Am I in Notepad now?")


Alt-tab needs to be handled by a wrapper function that gives you the
object of the window you've switched to:

otherwin = alt_tab()
with otherwin:
...

If window is changed within 'with' block, the rest of block should be
ignored. Perhaps there could also be a way to switch this behaviour off,
for the entire script or for current block only.
What do you think of designs #3 and #4?

notepad_1 = start("Notepad")
notepad_2 = start("Notepad")
switch_to(notepad_1)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
switch_to(notepad_2)
press(CTRL + 'v')

notepad_1 = start("Notepad")
notepad_2 = start("Notepad")
notepad_1.activate()
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
notepad_2.activate()
press(CTRL + 'v')

I somehow prefer "activate" over "focus" as in my feeling, you'd
normally say that you focus *on* something, so it should be called
"focus_on" or "give_focus[_to]". Can you say, in everyday English, that
you "focus a window"? I'm not a native speaker so maybe my feeling is
misguided.


These are ok, too, but I feel it's much easier to send commands to a
wrong window vs. context managers. The same command in a different
window can have vastly different and dangerous effect. In other python
code that's generally not common at all, and would be bad style:

lst = lst1
lst.append('x')
del lst[3]
lst.insert(0, 'a')
lst = lst2
del lst[2]
lst.append('y')
lst = lst3
lst.insert(0, 'x')
lst += [1,2]


I think current window should also be acquired explicitly:

with get_current_window():
type("some kind of snippet")

For usage when a command should apply to all types of windows.

HTH, -m
 
N

Neil Cerutti

I think I would prefer context managers. I don't think it's a
big problem for win users because this behaviour would be one
of the first things documented in the start guide and would be
all over example scripts, so a new user missing or forgetting
it is not a realistic scenario.

If window focus switching is really a rarity, and only done
briefly then I agree that a context manager makes a nice and neat
solution.

But it's too powerful a generalisation for such a small corner
case.

Have you considered adding a keyword argument to each of your
global functions, which is normally None, but allows a user to
provide a prefered focus window?

enter_text("test.txt", focus=save_dialog)
press_button(Savebutton, focus=save_dialog)

(Those are just guesses at your API functions; sorry.)

When focus remains None, your usual assumptions about focus would
apply, otherwise the user preference overrides it.
 
M

Michael Herrmann

Am I the only one who appreciates the simplicity of


You are not the only one.

I suggest that you have a set of functions that work on "the current
window", whatever that is. Preferably there should always be a current
window, but if not, ensure that you give a clear error message.

This is exactly the way it is currently. I am glad you also see it that way..
Then you have syntax for operating on any named(?) window. So a user can
implicitly operate on the current window:

select(notepad)
write("goodbye cruel world")
save()

One idea would be to use the Window(...) constructor to select windows:

notepad = Window('Untitled - Notepad')
select(notepad)
save()

One advantage of using a global method to switch to a window is that this would allow you to directly switch to a Window without having to call Window(...):

switch_to('Untitled - Notepad')

I'm still not fully convinced of a global method though, for the reasons several people here have already mentioned.
or explicitly on any window they like:

excel.quit()

This example makes it look likely that there will have to be other operations that can be performed on windows (/running applications as Dave pointed out). So, a 'quit()' method that closes a window in addition to the alreadymentioned focus/select/activate method. This in turn makes the global function less attractive as once we start going down that route, global functions will proliferate.
I suggest you dig up an old book on "Hypercard", for Apple Macs in the
1980s and 90s. Back in the day, Macs could only run a single application
at a time, and Hypercard was limited to a single window at a time (called
a "stack"). But that stack (think: window) could have multiple
"cards" (think: window tabs), one of which was always current.
Hypercard's built-in programming language Hypertalk let you do things
like this:

go to stack "Notepad"
type "goodbye cruel world" in field "main" of card 7
click button "Save"
click button "Quit" of card "Main" of stack "Excel"

(more or less... it's been a few years since I've had a classic Mac
capable of running Hypercard.)

Very interesting. I had never heard of HyperCard. I read up on it a little and it sounds very similar to the model we are internally building of open applications (stacks) and their windows (cards). Also funny that HyperCard was one of Ward Cunningham's inspirations for coming up with the Wiki idea:http://c2.com/cgi/wiki?WikiWikiHyperCard

Thanks for this!

Michael
www.getautoma.com
 
M

Michael Herrmann

...

Also, it seems that in this thread, we are using "window" both to refer
to a particular application instance (like Notepad1 and Notepad2), and
to refer to windows within a single application.



Anyway, if you're only automating a few specific apps, you're not likely
to run into the problems that methods were intended to address. After
all, Notepad's bugs haven't seemed to change for a couple of decades, so
why should they fix anything now?

Having a selected window be an implied object for those global functions
yields at least the same problems as any writable global.
Multithreading, unexpected side effects from certain functions,
callbacks, etc.

As long as you know the program is going to be simple, pile on the
globals. But as soon as it advances, each of them is a trap to fall into.

You're right with everything you say. globals are bad and it may happen that this will bite me. I'm just not sure whether we should sacrifice the simpler syntax useful in say 80% of cases for something I'm not yet sure will ever become a real problem.
 
M

Michael Herrmann

If window focus switching is really a rarity, and only done
briefly then I agree that a context manager makes a nice and neat
solution.


But it's too powerful a generalisation for such a small corner
case.

Have you considered adding a keyword argument to each of your
global functions, which is normally None, but allows a user to
provide a prefered focus window?

enter_text("test.txt", focus=save_dialog)

press_button(Savebutton, focus=save_dialog)

It's an interesting new idea but I somehow feel it makes the existing functions too complicated. Also, having to add it to all existing, and future functions sounds a bit too cumbersome to me.
(Those are just guesses at your API functions; sorry.)

No worries! Thank you for your suggestion!

Michael
www.getautoma.com
 
M

Michael Herrmann

...
At the __exit__, further commands are no longer routed to that window;
if it was a nested context, window is switched to the outer context,
WHEN there are commands in it (i.e. on the first command). This seems
pretty intuitive to me:

with notepad1:
^S
with notepad2:
^S
write('something')
...
What I am most afraid of: that the window that's currently the
context "disappears":
notepad = start("Notepad")
with notepad:
press(ALT + TAB)
write("Am I in Notepad now?")


Alt-tab needs to be handled by a wrapper function that gives you the
object of the window you've switched to:

otherwin = alt_tab()
with otherwin:
...

If window is changed within 'with' block, the rest of block should be
ignored. Perhaps there could also be a way to switch this behaviour off,
for the entire script or for current block only.

What do you think of designs #3 and #4?
...

These are ok, too, but I feel it's much easier to send commands to a
wrong window vs. context managers. The same command in a different
window can have vastly different and dangerous effect. In other python
code that's generally not common at all, and would be bad style:

lst = lst1
lst.append('x')
del lst[3]
lst.insert(0, 'a')
lst = lst2
del lst[2]
lst.append('y')
lst = lst3
lst.insert(0, 'x')
lst += [1,2]

I think current window should also be acquired explicitly:

with get_current_window():
type("some kind of snippet")

For usage when a command should apply to all types of windows.

I was skeptical of your suggestion at first but trying it out on an example script made me see its appeal:

notepad_main = start("Notepad")
with notepad_main:
write("Hello World!")
save_dialogue = press(CTRL + 's')
with save_dialogue:
write("test.txt", into="File name")
click("Save")
click("Close")

Forcing the library user to always use the "with ..." seems like overkill though. I think the gained precision does not justify this burden on the library user. Hm....
 
C

Chris Angelico

save_dialogue = press(CTRL + 's')

Does every single API need to then consider the possibility of focus
changing? How does the press() function know that this will (or might
- if the file's already been named, Ctrl-S won't open a dlg) change
focus? How does the caller know?

ChrisA
 
D

Dave Angel

It's an interesting new idea but I somehow feel it makes the existing functions too complicated. Also, having to add it to all existing, and future functions sounds a bit too cumbersome to me.

Perhaps Neil didn't make it clear enough. I figure he meant a keyword
argument with an explicit default value of None. (or if you followed my
earlier discussion, default value of focused)

That way your user can keep using the functions for when there's no
ambiguity, but add a focus= parameter only when needed.

To go back to my sample wrapper functions, they'd look something like
(untested):


def write(*args, focus=focused):
focus.write(*args)

Of course, the user should only use the wrappers when things are sure to
remain "simple."
 
N

Neil Cerutti

Perhaps Neil didn't make it clear enough. I figure he meant a keyword
argument with an explicit default value of None. (or if you followed my
earlier discussion, default value of focused)

That way your user can keep using the functions for when there's no
ambiguity, but add a focus= parameter only when needed.

To go back to my sample wrapper functions, they'd look something like
(untested):


def write(*args, focus=focused):
focus.write(*args)

Of course, the user should only use the wrappers when things
are sure to remain "simple."

Yes, along those lines. Most code would never need to provide the
focus= keyword. Only when setting focus in a weird way would it
be needed.
 
M

Mitya Sirenef

...
At the __exit__, further commands are no longer routed to that window;
if it was a nested context, window is switched to the outer context,
WHEN there are commands in it (i.e. on the first command). This seems
pretty intuitive to me:

with notepad1:
^S
with notepad2:
^S
write('something')
...
What I am most afraid of: that the window that's currently the
context "disappears":
notepad = start("Notepad")
with notepad:
press(ALT + TAB)
write("Am I in Notepad now?")


Alt-tab needs to be handled by a wrapper function that gives you the
object of the window you've switched to:

otherwin = alt_tab()
with otherwin:
...

If window is changed within 'with' block, the rest of block should be
ignored. Perhaps there could also be a way to switch this behaviour off,
for the entire script or for current block only.

What do you think of designs #3 and #4?
...

These are ok, too, but I feel it's much easier to send commands to a
wrong window vs. context managers. The same command in a different
window can have vastly different and dangerous effect. In other python
code that's generally not common at all, and would be bad style:

lst = lst1
lst.append('x')
del lst[3]
lst.insert(0, 'a')
lst = lst2
del lst[2]
lst.append('y')
lst = lst3
lst.insert(0, 'x')
lst += [1,2]

I think current window should also be acquired explicitly:

with get_current_window():
type("some kind of snippet")

For usage when a command should apply to all types of windows.

I was skeptical of your suggestion at first but trying it out on an
example script made me see its appeal:
notepad_main = start("Notepad")
with notepad_main:
write("Hello World!")
save_dialogue = press(CTRL + 's')
with save_dialogue:
write("test.txt", into="File name")
click("Save")
click("Close")

Forcing the library user to always use the "with ..." seems like
overkill though. I think the gained precision does not justify this
burden on the library user. Hm....


I don't see why that's a big deal, I've used AHK extensively and in my
experience you don't switch windows all that often. I think it's best to
optimize to have easy to type and read commands while you're working in
the same window.

I think you could argue that dialogs that belong to the main window
should be handled implicitly, though. I think for other windows it'd
definitely be good to use context managers, but for quick/simple dialogs
it's too much hassle, although for large, complex dialogs that have
inner tabs and require a lot of work, it again starts to make sense.

At the very least, for small dialogs it's sipmpler to do:

with press(CTRL + 's'):
write("test.txt", into="File name")
click("Save")


-m


--
Lark's Tongue Guide to Python: http://lightbird.net/larks/

Calamities are of two kinds: misfortunes to ourselves, and good fortune
to others.
Ambrose Bierce, The Devil's Dictionary
 
S

Steven D'Aprano

You're right with everything you say. globals are bad and it may happen
that this will bite me.

Global *variables* are bad, not global functions. You have one global
variable, "the current window". So long as your API makes it obvious when
the current window changes, implicitly operating on the current window is
no more dangerous than Python's implicit operations on the current
namespace (e.g. "x = 2" binds 2 to x in the current namespace).

I recommend you look at the random.py API. You have a Random class, that
allows the user to generate as many independent random number generators
as needed. And the module also initialises a private instance, and
exposes the methods of that instance as top-level functions, to cover the
90% simple case where your application only cares about a single RNG.
 
M

Michael Herrmann

Does every single API need to then consider the possibility of focus
changing? How does the press() function know that this will (or might
- if the file's already been named, Ctrl-S won't open a dlg) change
focus? How does the caller know?

While I can see where it is coming from, I am also not a big fan of this idea.

Michael
 
M

Michael Herrmann

Perhaps Neil didn't make it clear enough. I figure he meant a keyword
argument with an explicit default value of None. (or if you followed my
earlier discussion, default value of focused)

That way your user can keep using the functions for when there's no
ambiguity, but add a focus= parameter only when needed.

To go back to my sample wrapper functions, they'd look something like
(untested):

def write(*args, focus=focused):
focus.write(*args)

I understood what you meant - I'm not so worried about the invocations, as of course the parameter can be omitted if there's a default value/behaviour.. What I am worried about is the complexity this approach adds to several functions. Yes, you could argue that one keyword argument really isn't that much, but then you have to maintain and document it for all functions that have the new keyword parameter. In other words, a single functionality thatis not needed 90% of the time increases the complexity of several, not really related functions. I am very grateful for your suggestions! But I don'tthink adding this keyword parameter is the way to go for us.

Thanks,
Michael
www.getautoma.com
 
M

Michael Herrmann

overkill though. I think the gained precision does not justify this
burden on the library user. Hm....

I don't see why that's a big deal, I've used AHK extensively and in my
experience you don't switch windows all that often. I think it's best to
optimize to have easy to type and read commands while you're working in
the same window.

I think you could argue that dialogs that belong to the main window
should be handled implicitly, though. I think for other windows it'd
definitely be good to use context managers, but for quick/simple dialogs
it's too much hassle, although for large, complex dialogs that have
inner tabs and require a lot of work, it again starts to make sense.

At the very least, for small dialogs it's sipmpler to do:

with press(CTRL + 's'):
write("test.txt", into="File name")
click("Save")

I think what the context manager approach really has going for itself is the syntactic structure it gives to scripts, that makes it easy to see what is going on in which window. Semantically, however, I think the fit of this approach has some rough edges: The fact that there needs to be some specialtreatment for ALT + TAB, that actions such as `press` "sometimes" return values that are needed to continue the script and so on. It really has its appeal, but I think it's a bit too special and intricate to be used by a broad audience.
Calamities are of two kinds: misfortunes to ourselves, and good fortune
to others.

;-)

Michael
www.getautoma.com
 
M

Michael Herrmann

Global *variables* are bad, not global functions. You have one global
variable, "the current window". So long as your API makes it obvious when
the current window changes, implicitly operating on the current window is
no more dangerous than Python's implicit operations on the current
namespace (e.g. "x = 2" binds 2 to x in the current namespace).

I'm generally wary of everything global, but you're right as long as no (global) state is involved.
I recommend you look at the random.py API. You have a Random class, that
allows the user to generate as many independent random number generators
as needed. And the module also initialises a private instance, and
exposes the methods of that instance as top-level functions, to cover the
90% simple case where your application only cares about a single RNG.

I looked it up - I think this is a very good approach; to provide easy access to the functionality used in 90% of cases but still give users the flexibility to cover the edge cases.

After everybody's input, I think Design #2 or Design #4 would be the best fit for us:

Design #2:
notepad_1 = start("Notepad")
notepad_2 = start("Notepad")
switch_to(notepad_1)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
switch_to(notepad_2)
press(CTRL + 'v')

Design #4:
notepad_1 = start("Notepad")
notepad_2 = start("Notepad")
notepad_1.activate()
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
notepad_2.activate()
press(CTRL + 'v')

Normally, I'd go for Design #4, as it results in one less global, is betterfor autocompletion etc. The thing with our library is that it tries to make its scripts as similar as possible to giving instructions to someone looking over their shoulder at a screen. And in this situation you would just say

activate(notepad)

rather than

notepad.activate().

So the problem lies in a difference between Python's and English grammar. For beauty, I should go with #2. For pragmatism, I should go with #4. It hurts, but I'm leaning towards #4. I have to think about it a little.

Thank you so much to everybody for your inputs so far!
Best,
Michael
www.getautoma.com
 
C

Chris Angelico

I understood what you meant - I'm not so worried about the invocations, as of course the parameter can be omitted if there's a default value/behaviour. What I am worried about is the complexity this approach adds to severalfunctions. Yes, you could argue that one keyword argument really isn't that much, but then you have to maintain and document it for all functions that have the new keyword parameter. In other words, a single functionality that is not needed 90% of the time increases the complexity of several, not really related functions. I am very grateful for your suggestions! But I don't think adding this keyword parameter is the way to go for us.

Not seeking to advocate this particular option, but it would be
possible to make a single wrapper for all your functions to handle the
focus= parameter:

def focusable(func):
@functools.wraps(func)
def wrapper(*args,focus=None):
if focus: focus.activate()
return func(*args)
return wrapper

Then you just decorate all your functions with that:
def write(string):
# do something with the active window

ChrisA
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
474,073
Messages
2,570,539
Members
47,197
Latest member
NDTShavonn

Latest Threads

Top