Python coding

Strings

Todo

Documentation needed for string coding rules:

  • Differenciate strings and byte-strings:
    • Use of "" / r"" / f"" (double quote) to enclose str strings
      • Except for strings in f-string {}-blocks.

    • Use of b'' / rb'' (simple quotes) to enclose bytes strings

  • Use f-strings
    • Except for debugging (for optimization concerns)

    • Except for assertion errors and evidence (for optimization concerns)

    • Except for regex (because of ‘{}’ escaping)

    • Except for bytes (f-strings not available)

  • Use repr specification (f"{...!r}" / "%r") instead of calling repr() (for optimization concerns)

Namings

Todo

Documentation needed for namings:

  • PEP8 compatible

  • Packages

  • Modules

  • Classes

  • Attributes

  • Methods & functions

  • Getters (properties) and setters, same as attributes

  • Constants

Presentation

Todo

Documentation needed for code presentation

  • Indentation:

    • Avoid right-aligned comments (hard to maintain when the names change)

    • Functions and methods (same purpose):

      • New line for the first parameter

      • Parameters indented with 2 tabs (as proposed by PyCharm by default). Makes it more readable by differenciating the parameters from the function / body.

  • Trailing commas (refer to PEP8 https://www.python.org/dev/peps/pep-0008/#when-to-use-trailing-commas)

  • New line after the opening parenthesis of the function declarations

Packages

Todo

Documentation needed for packages, file names:

  • scenario package

  • ‘__init__.py’ that exports symbols from the given package

Note

scenario.test, scenario.tools subpackages are implemented at different locations, out of the main ‘src/’ directory.

Static & class methods

Do not use the @staticmethod or @classmethod whenever a method could be converted so. It is preferrable to rely on the meaning at first, in order to make the code more stable along the time.

By default, PyCharm “detects any methods which may safely be made static”. This coding rule goes against these suggestions. By the way, we do not want to set # noinspection PyMethodMayBeStatic pragmas everywhere a method could “be made static” in the code. Thus, please disable the “Method may be static” inspection rule in your PyCharm settings.

Typings

The code uses Python 2 typings.

Even though Python 2 compatibility is not expected, it has proved that comment type hints did not suffer a couple of limitations compared with Python 3 type hints:

  • impossible for a method to return an instance of the current class, without a prior import __future__ import annotations statement, which is available from Python 3.8 only,

    # Python 3 type hints
    
    from __future__ import annotations  # << Python 3.8 only
    
    class A:
        def meth() -> A:  # << NameError: name 'A' is not defined, if `__future__.annotations` not imported
            return A()
    
    # Python 2 type hints
    
    class A:
        def meth():  # type: (...) -> A
            return A()
    
  • impossible to define the type of the iterating variable in a for loop statement,

    # Python 3 type hints
    
    for _i: int in range(10):  # << SyntaxError: invalid syntax
        pass
    
    # Python 2 type hints
    
    for _i in range(10):  # type: int
        pass
    
  • impossible to define the type of a couple of variables initialized from a function returning a tuple.

    # Python 3 type hints
    
    _a: int, _b: str = func()  # << SyntaxError: invalid syntax
    
    # Python 2 type hints
    
    _a, _b = func()  # type: int, str
    

Furthermore, we use the multi-line style, that makes the code diffs shorter, and the maintainance simpler by the way.

Imports

Order of Imports

  1. Place system imports at first:

  • One system import per import line.

  • Sort imports in the alphabetical order.

  1. Then scenario imports:

  • Use the relative form from .modulename import ClassName.

  • Sort scenario imports in the alphabetical order of module names.

  • For a given module import statement, sort the symbols imported in their alphabetical order as well.

  1. Then test imports (for test modules only).

Justification for ordering imports

Giving an order for imports is a matter of code stability.

The alphabetical order works in almost any situation (except on vary rare occasion). It’s simple, and easy to read through. That’s the reason why it is chosen as the main order for imports.

The fewer project imports at the top level

We call project imports imports from modules located in the same package. As a consequence, these imports are usually the relative ones.

In order to avoid cyclic module dependencies in the package, the fewer project imports shall be placed at the top level of modules. Postpone as much as possible the imports with local imports in function or methods where the symbol is actually used.

Nevertheless, this does not mean that local imports should be repeated every time that a basic object is used in each function of method (like Path for instance).

In order to ensure that a top level project import is legitimate, it shall be justified with a comment at the end of the import line. typing.TYPE_CHECKING import statements shall be justified as well (see the typing.TYPE_CHECKING section below).

The ‘tools/checkdeps.py’ script may help visualizing scenario module dependencies:

$ ./tools/checkdeps.py
INFO     <13>               __init__.py => [actionresultdefinition.py, actionresultexecution.py, args.py, assertionhelpers.py,
INFO                                        assertions.py, campaignargs.py, campaignexecution.py, campaignreport.py,
INFO                                        campaignrunner.py, configdb.py, confignode.py, console.py, datetimeutils.py,
INFO                                        debugutils.py, enumutils.py, errcodes.py, executionstatus.py, handlers.py,
INFO                                        issuelevels.py, knownissues.py, locations.py, logextradata.py, logger.py,
INFO                                        loggermain.py, path.py, pkginfo.py, scenarioargs.py, scenarioconfig.py,
INFO                                        scenariodefinition.py, scenarioevents.py, scenarioexecution.py, scenarioreport.py,
INFO                                        scenariorunner.py, scenariostack.py, stats.py, stepdefinition.py, stepexecution.py,
INFO                                        subprocess.py, testerrors.py, timezoneutils.py]
INFO     <12>        campaignlogging.py => [campaignexecution.py, enumutils.py]
INFO     <12>         campaignreport.py => [campaignexecution.py, logger.py, path.py, xmlutils.py]
INFO     <12>         campaignrunner.py => [campaignexecution.py, errcodes.py, logger.py]
INFO     <12>         scenarioevents.py => [campaignexecution.py, enumutils.py, scenariodefinition.py, stepdefinition.py,
INFO                                        testerrors.py]
INFO     <11>      campaignexecution.py => [executionstatus.py, path.py, scenarioexecution.py, stats.py, testerrors.py]
INFO     <11>        scenariologging.py => [actionresultdefinition.py, enumutils.py, scenariodefinition.py,
INFO                                        scenarioexecution.py, stepdefinition.py, stepsection.py, testerrors.py]
INFO     <11>         scenarioreport.py => [actionresultdefinition.py, logger.py, scenarioexecution.py, stepdefinition.py]
INFO     <11>        scenarioresults.py => [logger.py, scenarioexecution.py, testerrors.py]
INFO     <11>          scenariostack.py => [actionresultdefinition.py, actionresultexecution.py, logger.py,
INFO                                        scenariodefinition.py, scenarioexecution.py, stepdefinition.py, stepexecution.py,
INFO                                        stepuserapi.py]
INFO     <10>               handlers.py => [logger.py, scenariodefinition.py]
INFO     <10>      scenarioexecution.py => [executionstatus.py, scenariodefinition.py, stats.py, stepdefinition.py]
INFO     <10>         scenariorunner.py => [actionresultdefinition.py, enumutils.py, errcodes.py, knownissues.py, logger.py,
INFO                                        scenariodefinition.py, stepdefinition.py, stepuserapi.py, testerrors.py]
INFO     <09>     scenariodefinition.py => [assertions.py, logger.py, stepdefinition.py, stepsection.py, stepuserapi.py]
INFO     <08>          stepexecution.py => [actionresultdefinition.py, stats.py, stepdefinition.py]
INFO     <08>            stepsection.py => [stepdefinition.py]
INFO     <07>         stepdefinition.py => [actionresultdefinition.py, assertions.py, knownissues.py, locations.py, logger.py,
INFO                                        stepuserapi.py]
INFO     <06>  actionresultexecution.py => [actionresultdefinition.py]
INFO     <06>           campaignargs.py => [args.py, path.py, scenarioargs.py]
INFO     <06>            knownissues.py => [logger.py, testerrors.py]
INFO     <05> actionresultdefinition.py => [enumutils.py, locations.py]
INFO     <05>           scenarioargs.py => [args.py, subprocess.py]
INFO     <05>             testerrors.py => [locations.py, logger.py]
INFO     <04>                   args.py => [configargs.py, logger.py, loggingargs.py]
INFO     <04>               configdb.py => [confignode.py, enumutils.py, logger.py]
INFO     <04>           debugloggers.py => [debugutils.py, logger.py]
INFO     <04>              locations.py => [logger.py]
INFO     <04>             logfilters.py => [logger.py]
INFO     <04>             loggermain.py => [logger.py]
INFO     <04>                 reflex.py => [debugclasses.py, logger.py]
INFO     <04>             subprocess.py => [errcodes.py, logger.py]
INFO     <04>          testsuitefile.py => [logger.py]
INFO     <03>           logformatter.py => [console.py, logextradata.py]
INFO     <03>                 logger.py => [console.py, logextradata.py]
INFO     <02>             assertions.py => [assertionhelpers.py]
INFO     <02>           debugclasses.py => [enumutils.py]
INFO     <02>        executionstatus.py => [enumutils.py]
INFO     <02>           logextradata.py => [enumutils.py]
INFO     <02>         scenarioconfig.py => [confignode.py, console.py, enumutils.py, path.py]
INFO     <01>       assertionhelpers.py => []
INFO     <01>             configargs.py => []
INFO     <01>              configini.py => []
INFO     <01>             configjson.py => []
INFO     <01>              configkey.py => []
INFO     <01>             confignode.py => []
INFO     <01>            configtypes.py => []
INFO     <01>             configyaml.py => []
INFO     <01>                console.py => []
INFO     <01>          datetimeutils.py => []
INFO     <01>             debugutils.py => []
INFO     <01>              enumutils.py => []
INFO     <01>               errcodes.py => []
INFO     <01>            issuelevels.py => []
INFO     <01>            loggingargs.py => []
INFO     <01>         loggingservice.py => []
INFO     <01>             loghandler.py => []
INFO     <01>                   path.py => []
INFO     <01>                pkginfo.py => []
INFO     <01>                  stats.py => []
INFO     <01>            stepuserapi.py => []
INFO     <01>               textfile.py => []
INFO     <01>          timezoneutils.py => []
INFO     <01>                 typing.py => []
INFO     <01>               xmlutils.py => []

typing.TYPE_CHECKING pattern

See https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports for some information on this so-called pattern in this section.

Use the typing.TYPE_CHECKING pattern for two reasons only:

  1. for types declared only when typing.TYPE_CHECKING is True of course (otherwise the code execution will fail),

  2. but then, for cyclic type hint dependencies only.

Risks with the if typing.TYPE_CHECKING: pattern

Sometimes, a class A requires another class B (in one of its method signatures, or possibly because it inherits from B), and so does the other class B with class A as well.

From the execution point of view, this situation can usually be handled with local imports in some of the methods involved.

Still, from the type hint point of view, a cyclic dependency remains between the two modules. The typing.TYPE_CHECKING pattern makes it possible to handle such cyclic dependencies.

But caution! the use of this pattern generates a risk on the execution side. Making an import under an if typing.TYPE_CHECKING: condition at the top of a module makes the type checking pass. Nevertheless, the same import should not be forgotten in the method(s) where the cyclic dependency is actually used, otherwise it fails when executed, which is somewhat counterproductive regarding the type checking goals!

Let’s illustrate that point with an example.

Let the a.py module define a super class A with a getb() method returning a B instance or None:

if typing.TYPE_CHECKING:
    from .b import B

class A:
    def getb(self, name):  # type: (str) -> typing.Optional[B]
        for _item in self.items:
            if isinstance(_item, B):
                return _item
        return None

Let the b.py module define B, a subclass of A:

from .a import A

class B(A):
    def __init__(self):
        A.__init__(self)

The B class depends on the A class for type hints and execution. So the from .a import A import statement must be set at the top of the b.py module.

The A class needs the B class for the signature of its A.getb() method only. Thus, the from .b import B import statement is set at the top of the a.py module, but under a if typing.TYPE_CHECKING: condition.

This makes type checking pass, but fails when the A.getb() method is executed. Indeed, in a.py, as the B class is imported for type checking only, the class is not defined when the isinstance() call is made. By the way, the import statement must be repeated as a local import when the B class is actually used in the A.getb() method:

class A:
    def getb(self, name):  # type: (str) -> typing.Optional[B]
        # Do not forget the local import!
        from .b import B

        for _item in self.items:
            if isinstance(_item, B):
                return _item
        return None

That’s the reason why the typing.TYPE_CHECKING pattern shall be used as less as possible, i.e. when cyclic dependencies occur because type hints impose it.

Python compatibility

The code supports Python versions from 3.6.

The ‘tools/checktypes.py’ scripts checks code amongst Python 3.6.

Python versions

No need to handle Python 2 anymore, as long as its end-of-line was set on 2020-01-01 (see PEP 373).

As of 2021/09, Python 3.6’s end-of-life has not been declared yet (see https://devguide.python.org/devcycle/#end-of-life-branches), while Python 3.5’s end-of-life was set on 2020-09-30 (see PEP 478).