Python coding¶
Strings¶
Todo
Documentation needed for string coding rules:
- Differenciate strings and byte-strings:
- Use of
""
/r""
/f""
(double quote) to enclosestr
strings Except for strings in f-string {}-blocks.
- Use of
Use of
b''
/rb''
(simple quotes) to enclosebytes
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 callingrepr()
(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¶
Place system imports at first:
One system import per
import
line.Sort imports in the alphabetical order.
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.
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:
for types declared only when
typing.TYPE_CHECKING
isTrue
of course (otherwise the code execution will fail),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).