#Would that `if` statement also confuse

1 messages · Page 1 of 1 (latest)

cobalt void
#

Yes, that is one of the exceptions. Another you might see is

if TYPE_CHECKING:  # or if.typing.TYPE_CHECKING ...
  # usually imports for type hints that would othewise cause circular import problems

but that won't appear as often in CircuitPython code (unless you're looking at my code ...)

#

Basically. you don't want any "flow control logic" (if/for/while/try...except/...) at the global scope doing anything other than setting up the functions, classes, and global variables in a .py file. Whatever that module is supposed to do should (almost) always be done by calling the functions and/or instantiating classes defined in that file.

#

In most cases, this will be because your module (.py file) gets imported from another .py file which needs the functionality your module provides.

#

The

if __name__ == "__main__":
  # this is being run directly as a stand-alone script, so
  # extra logic for that special case goes here.
  # for example...
  import sys
  if len( sys.argv ) > 1 and sys.argv[1] == "--help":
    print( "help goes here..." )
  else:
    danceTheLightFantastic(sys.argv[])

idiom is a way to avoid needing a separate trivial "main" .py file to use your module's functionality directly from the command line (i.e. python myFile.py hello world) without causing problems when another module imports yours instead.

cobalt void
#

One other thing to avoid is global scope print(...) statements. Those aren't so bad when running your .py file directly, but can cause headaches when your file is imported. If you need some "global" prints to help figure out problems while debugging or modifying your source, it's best to at least use something like

# ...
__debuggingGlobal = False
def __debugPrint( msg ):
  if __debuggingGlobal: print( message )
# ...

__debugPrint( "about to define class Foo" )
class Foo(object):
  # ...

and set __debuggingGlobal to True only when you need to see global prints

rapid talon
#

This is really interesting, thank you for the detailed response!

#

Though I might not be fully understanding: does all of this also apply to files that are not libraries/modules? The while True code block I had on the global scope was part of the code.py file CircuitPython executes, not a library to be normally imported.

#

Sphinx does import it, which is why I understand that I will need to check if __name__ == "__main__", but would it still be recommended in this case not to do anything on the global scope?

cobalt void
#

Basically, it applies to any python file which you want to be able to do more than just "run". There are all kinds of useful tools for enhancing your Python development experience. Documentation generators like Sphinx are just one example. Many of those tools work their magic by importing the file into Python and then using all the nifty options Python provides for inspecting what they loaded. If there's something like a while true: at global scope, the import get stuck waiting on a loop that may never complete.

#

So if you've got something really simple that's only intended to be run directly (never imported) - for example a code.py that's only a few dozen lines - global loops might be ok.

cobalt void
#

Here's a simple example:

class RGB(object):
    # ...
    
    @staticmethod
    def fromString( color:str ) -> RGB: ...

for _colorName, _rgbValue in {
        "BLACK": "#000000",
        "WHITE": "#FFFFFF",
        "RED": "#FF0000",
        "BLUE": "#0000FF",
        "GREEN": "#00FF00",
}.items():
    setattr( RGB, _colorName, RGB.fromString(_rgbValue) )

This adds a bunch of colors to the RGB class, so if you need red you can simply use RGB.RED

#

(note that this is just an example of the way you might legitimately use a "global" loop. But it's not a particularly clean way to add class attributes)

rapid talon
#

@cobalt void And won't making such big parts of the code inaccessible to say, Sphinx's Autodoc, create any other side effects? Or could it still obtain all the information of the code it needs from the main() function's parameters and return type?

#

Though I guess questions like this one start to fall outside of the scope of my project, but it's interesting nonetheless.

rapid talon
#

~~ Also, can I do


def setup():
    # Setup code here

def loop():
    # Code to be looped over here

if __name__ == "__main__":
    setup()
    while True:
        loop()

or should I stick to


def setup():
    # Setup code here

def loop():
    while True:
        # Code to be looped over here

if __name__ == "__main__":
    setup()
    loop()

?
I feel like the second approach is more in line with what you've told me, but the first seems a bit cleaner? ~~

rapid talon
#

~~ Or I guess another way would be

def main():
    # Setup code here
    while True:
        # Code to be looped over here

if __name__ == "__main__":
    main()
``` ~~
#

I just remembered that you said I can still define global variables at the global scope, so I can probably do everything the setup() function should do on the global scope. I guess the question then would be whether to do

# Setup code here
 
def main():
    # Code to be looped over here

if __name__ == "__main__":
    while True:
        main()

or

# Setup code here

def main():
    while True:
        # Code to be looped over here

if __name__ == "__main__":
    main()
cobalt void
#

Personally I'd choose the second (while loop inside main function body). Practically, in this special case ( loops inside the if __name__ == "__main__": block) it probably doesn't matter as far as any code analysis tools are concerned. Even those that actually import your code won't end up running anything in the the if __name__ == "__main__": block.

cobalt void
# rapid talon Though I guess questions like this one start to fall outside of the scope of my ...

What parts of the code would be "inaccessible"? autodoc actually imports the modules it's analyzing, so it would "see" the colors as class scope properties and generate documentation for them. It would be just as if you had said

class RGB:
  # ....
  
  BLACK = RGB( "#000000" )
  WHITE = RGB( "#FFFFFF" )

except that this code isn't actually valid - if you tried you would find that the RGB class isn't "defined yet" at that point. You can use (create instances) of a class inside the scope of that class's methods definitions - because they don't actually get called until after the class is "completed". But you can't use it to initialize class member variables because the class isn't actually available yet.

#

But in general, if you're thinking that playing tricks like this to load class attributes "after the fact" can end up causing issues with code analysis tools, documentation generators, etc... you're not wrong - it can be a slippery slope. It can also lead to some very subtle, hard to find bugs and make things harder to maintain over time.

rapid talon
# cobalt void What parts of the code would be "inaccessible"? **autodoc** actually imports th...

It wouldn't see anything inside of the main() function, right? My guess is that you probably never have anything to document there, but again, I don't know exactly what autodoc is looking for when it imports the file. For instance, if your example were instead:

class RGB(object):
    # ...
    
    @staticmethod
    def fromString( color:str ) -> RGB: ...

def main():
    for _colorName, _rgbValue in {
            "BLACK": "#000000",
            "WHITE": "#FFFFFF",
            "RED": "#FF0000",
            "BLUE": "#0000FF",
            "GREEN": "#00FF00",
    }.items():
        setattr( RGB, _colorName, RGB.fromString(_rgbValue) )

if __name__ == "__main__":
    main()

I'd assume in that case it wouldn't be as if you'd said

class RGB:
  # ....
  
  BLACK = RGB( "#000000" )
  WHITE = RGB( "#FFFFFF" )
rapid talon
#

I'm not entirely sure why, but I was trying to instantiate an object of this class in the global scope and Sphinx was getting stuck there as well. I fixed it by moving it into the main() function, but I still find it weird.

class Accelerometer:
    def __init__(self) -> None:
        self.lis3dh = adafruit_lis3dh.LIS3DH_I2C(board.I2C())
        self.lis3dh.range = adafruit_lis3dh.RANGE_4_G

#

I guess maybe it's because of the board.I2C() method?

cobalt void
#

That sounds right. Any functions you call or classes you instantiate directly within the global scope (outside of if __name__ == "__main__":) that have loops or call things which might throw block or throw an exception can also cause problems just as it would at global scope.

cobalt void
#

In general, I try and avoid putting anything other than imports, or function/class definitions at global scope. I use a class to hold anything that would otherwise go in global variables. Global variables can be convenient for short, simple projects - less complicated than wrapping everything in a class and having to use self. everywhere - but they can cause lots of grief as a project grows more complex.

#

If you put this (warning: haven't actually tested this yet, but I'm fairly sure it'll work) in ProjectBase.py :

#############################################################################
# Project base class
class ProjectBase(object):
    def __init__(self,verbose:bool = False) -> None:
        self.verbose = verbose

    def sayAtStartup(self, message: str) -> None:
        if self.verbose:
            print(message)

    def sayWhileRunning(self, message: str) -> None:
        if self.verbose:
            print(message)
    
    def setup(self) -> None:
        raise NotImplementedError("Setup method must be implemented in the subclass")

    def loop(self) -> None:
        raise NotImplementedError("Loop method must be implemented in the subclass")

    def run(self) -> None:
        self.setup()
        self.sayAtStartup("starting mail loop")
        while True:
            self.loop()

#

then you could use the following in code.py :

import board
import busio
import adafruit_lis3dh
from ProjectBase import ProjectBase

#############################################################################
# Accelerometer wrapper class
class Accelerometer:
    def __init__(self, i2c:busio.I2C) -> None:
        # generally, any time you create your own class which uses
        # an I2C device, you should pass the I2C object to it instead
        # of calling `board.I2C()`.
        self.lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)
        self.lis3dh.range = adafruit_lis3dh.RANGE_4_G

    @property
    def acceleration(self) -> tuple[float, float, float]:
        return self.lis3dh.acceleration

#############################################################################
# Main project class
class MyProject(ProjectBase):
    
    def setup(self) -> None:
        self.sayAtStartup("Setting up the project...")

        self.i2c = board.I2C()  # Initialize I2C
        self.accelerometer = Accelerometer(self.i2c)

    def loop(self) -> None:
        print( f"Acceleration: {self.accelerometer.acceleration}" )

#############################################################################
if __name__ == "__main__":
    project = MyProject(verbose=True)
    project.run()
#

And then, since you're initializing your "global" bits in MyProjject.setup() instead of the global scope, it won't cause any problems for Sphinx or other code analysis tools.

#

(note that there are exceptions where instantiating objects at global scope is reasonable or even desirable, but those are exceptions and it's generally easier to just avoid global instances entirely unless you're building some fairly advanced stuff)

#

(and technically, import someModule, def myFunction():, and class MyClass_(...): in the global scope are actually "instantiating objects at global scope" - basically everything you can define/name in Python is an object)

#

(and I should probably mention that I'm currently experimenting with a system where everything in code.py is instantiated at global scope - but it's a very special purpose system)

#

(probably best to IGNORE EVERYTHING I JUST SAID IN (PARENTHESIS) and just avoid instantiating / calling anything in global scope)

rapid talon
cobalt void
#

Thanks, I hope it's useful. It's essentially a trimmed down version of what I use for some of my own projects. There are some specific reasons why it's structured that way. For example, the separation of "setup" and "loop" may be familiar if you've seen Arduino code.

#

Actually there is at least one "mistake" - the ProjectBase def loop(self): should probably be def loopOnce(self): instead.

In Arduino coding, your "loop" is expected to simply loop forever, but that can cause problems if you're not planning ahead. Explicitly defining your "one pass through the main loop" behavior is another little twist that can end up paying dividends later. That way you can cleanly expand the ProjectBase.run(...) method if you ever want to add things like configurable sleep timing between loops, asyncio (especially if you're using network bits like adafruit_httpserver), etc...

rapid talon
#

@cobalt void I am now refactoring my code to follow your recommendations thanks again for them. How do you usually deal with hardware-related constants such as pinouts? Do you leave them in the global scope, as MyProject class constants or do you set them up in setup()?