Friday Finking: 'main-lines' are best kept short
DL Neil
PythonList at DancesWithMice.info
Thu Sep 12 23:58:17 EDT 2019
(this follows some feedback from the recent thread: "WedWonder: Scripts
and Modules" and commences a somewhat-related topic/invitation to
debate/correct/educate)
Is it a good idea to keep a system's main-line* code as short as
possible, essentially consigning all of 'the action' to application and
external packages and modules?
* my choice of term: "main-line", may be taken to mean:
- the contents of main(),
- the 'then clause' of an if __name__ == __main__: construct,
- a __main__.py script.
In a previous thread I related some ?good, old days stories. When we
tried to break monolithic programs down into modular units, a 'rule of
thumb' was "one page-length" per module (back in the mainframe days our
code was 'displayed' on lineflo(w) (continuous stationery) which was 66
- call it 60, lines per page - and back-then we could force a page-break
where it suited us!). Then when we moved to time-share screens (80
characters by 24 lines), we thought that a good module-length should
conform to screen-size. These days I have a large screen mounted in
'portrait mode', so on that basis I'm probably back to 50~60 lines (yes,
these old eyes prefer a larger font - cue yet more cheeky, age-ist
comments coming from my colleagues...)
Likely I have also picked-up and taken-to-heart the *nix mantra of code
doing 'one job, and doing it well' (and hence the extensive powers of
redirects, piping, etc - in Python we 'chain' code-units together with
"import"). Accordingly, I tend to err on the side of short units of
code, and thus more methods/functions than others might write.
In "Mastering Object-oriented Python" the author discusses "Designing a
main script and the __main__ module" (Ch17):
<<<
A top-level main script will execute our application. In some cases, we
may have multiple main scripts because our application does several
things. We have three general approaches to writing the top-level main
script:
• For very small applications, we can run the application with python3.3
some_script.py . This is the style that we've shown you in most examples.
• For some larger applications, we'll have one or more files that we
mark as executable with the OS chmod +x command. We can put these
executable files into Python's scripts directory with our setup.py
installation. We run these applications with some_script.py at the
command line.
• For complex applications, we might add a __main__.py module in the
application's package. To provide a tidy interface, the standard library
offers the runpy module and the -m command-line option that will use
this specially named module. We can run this with python3.3 -m some_app.
[explanation of "shebang" line - the second approach, above]
Creating a __main__ module
To work with the runpy interface, we have a simple implementation. We
add a small __main__.py module to our application's top-level package.
We have emphasized the design of this top-level executable script file.
We should always permit refactoring an application to build a larger,
more sophisticated composite application. If there's functionality
buried in __main__.py , we need to pull this into a module with a clear,
importable name so that it can be used by other applications.
A __main__.py module should be something small like the following code:
import simulation
with simulation.Logging_Config():
with simulation.Application_Config() as config:
main= simulation.Simulate_Command()
main.config= config
main.run()
We've done the minimum to create the working contexts for our
application. All of the real processing is imported from the package.
Also, we've assumed that this __main__.py module will never be imported.
This is about all that should be in a __main__ module. Our goal is to
maximize the reuse potential of our application.
[example]
We shouldn't need to create composite Python applications via the
command-line API. In order to create a sensible composition of the
existing applications, we might be forced to refactor stats/__main__.py
to remove any definitions from this module and push them up into the
package as a whole.
>>>
Doesn't the author thus suggest that the script (main-line of the
program) should be seen as non-importable?
Doesn't he also suggest that the script not contain anything that might
be re-usable?
Accordingly, the script calls packages/modules which are both importable
and re-usable.
None of which discounts the possibility of having other 'main-lines' to
execute sub-components of the (total) application, should that be
appropriate.
An issue with 'main-line' scripts is that they can become difficult to
test - or to build, using TDD and pytest (speaking personally). Pytest
is great for unit tests, and can be used for integration testing, but
the 'higher up' the testing pyramid we go, the less effectual it becomes
(please don't shoot me, pytest is still an indispensable tool!)
Accordingly, if 'the action' is pushed up/out to modules, this will ease
the testing, by access and by context!
To round things out, I seem to be structuring projects as:
.projectV2
-- README
-- LICENSE
-- docs (sub-directory)
-- .git (sub-directory)
-- etc
-- __main__.py
-- project (some prefer "src") sub-directory
-- -- package directory/ies
-- -- modules directory/ies
(obviously others will be import-ed from wherever pip3 (etc) installed them)
-- test (sub-directory)
-- -- test modules
(although sometimes it seems easier to add a test sub-directory to the
individual package directory, above)
I like the idea that whilst coding, the editor only needs to show the
project sub-directory, because (much of) the rest is irrelevant - I have
a separate window for the test code, and thus the same distraction-free
virtue also applies.
Part of making the top-level "projectV2" directory almost-irrelevant in
day-to-day dev-work is that __main__.py contains very little, typically
three stages:
1 config (including start logging, etc, as appropriate)
2 create the applications central/action object
3 terminate
Nary an if __name__ == __main__ in sight (per my last "Wednesday
Wondering"), because "the plan" says there is zero likelihood of the
"main-line" being treated as a (re-usable) module! (and any refactoring
would, in any case, involve pushing such code out to a (re-usable) module!
When it comes to execution, the command (excluding any switches/options)
becomes:
[~/Projects]$ python3 projectV2
Which would also distinguish between project-versions, if relevant. More
importantly, changes to application version numbers do not require any
changes to import statements! (and when users don't wish to be expected
to remember version numbers "as well", use symlinks - just as we do with
python/python2/python3/python3.7...
Note that it has become unnecessary to add the -m switch!
Accordingly, very short entry-point scripts have been working for me.
(recognising that Python is used in many more application-areas than do I!)
Do you see a reason/circumstance when this practice might break-down?
Ref:
Mastering Object-oriented Python, S Lott
Copyright © 2014 Packt Publishing
--
Regards,
=dn
More information about the Python-list
mailing list