#!/usr/bin/env python3 """ ================================================================= This MODULE is used as a reference for Python source code style & documentation VERSION 24.02.2022 This file conforms to the Sphinx Napoleon syntax : https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html Ref : - Sphinx generalities on RST format : https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html - Sphinx autodoc : https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html - Sphinx autodocsumm : https://autodocsumm.readthedocs.io/en/latest/index.html - Sphinx autosummary : https://www.sphinx-doc.org/en/master/usage/extensions/autosummary.html - Sphinx apidoc (1) for autogeneration of files in the source folder : https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html - Sphinx apidoc (2) more explanations : https://samnicholls.net/2016/06/15/how-to-sphinx-readthedocs - Sphinx inheritance diagrams : https://www.sphinx-doc.org/en/master/usage/extensions/inheritance.html - Typehints cheatsheet : https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html - Doctests : https://docs.python.org/fr/3/library/doctest.html - Mixins : https://www.thedigitalcatonline.com/blog/2020/03/27/mixin-classes-in-python - Data Classes (3.7+) : https://realpython.com/python-data-classes Changes : - added new type annotation : Literal - added new types : NamedTuple, Dataclass, Enum, TypedDict - added custom and derived Exceptions ================================================================= """ # This import MUST BE at the very beginning of the file (or syntax error...) from __future__ import annotations from dataclasses import dataclass, field # TODO : # - exception (custom) # - dataclass # - NamedTuple # - Enum # ''' # ================================================================= # PACKAGES IMPORT # ================================================================= # ''' # --- 1) GENERAL PURPOSE IMPORTS --- from typing import Dict, List, Tuple, Sequence, Any, TypeVar, Union, Callable, Literal import typing import platform from datetime import date import random # --- 2) PROJECT SPECIFIC IMPORTS --- # from django.conf import settings as djangosettings # from common.models import AgentSurvey, AgentCmd, AgentLogs # from src.core.pyros_django.obsconfig.configpyros import ConfigPyros # from device_controller.abstract_component.device_controller import ( # DCCNotFoundException, UnknownGenericCmdException, UnimplementedGenericCmdException, UnknownNativeCmdException # ) # ''' # ================================================================= # GENERAL MODULE CONSTANTS & FUNCTIONS DEFINITIONS # ================================================================= # ''' # # - (General) Module level Constants # IS_WINDOWS = platform.system() == "Windows" DEBUG = False # - Typehint : Union (but not yet '|', only available with python 3.10) def general_function_that_returns_a_float( arg_a: int, arg_b: str | int, arg_c: float = 1.2, arg_d: bool = True ) -> float: """This function illustrates Typehint 'Union' (or '|') Args: arg_a: the path of the file to wrap arg_b: instance to wrap arg_c: toto arg_d: whether or not to delete the file when the File instance is destructed Returns: A buffered writable file descriptor Raises: AttributeError: The ``Raises`` section is a list of all exceptions that are relevant to the interface. ValueError: If `arg_a` is equal to `arg_b`. Usage: general_function_that_returns_a_float(arg_a=1, arg_b="toto") # => OK general_function_that_returns_a_float(arg_a=1, arg_b=1.2) # => KO (float not allowed) """ # comment on a a = 1 # comment on b b = 2 return 3.5 # - Typehint : Tuple (immutable) def general_function_that_returns_a_tuple_of_3_elem( a: int, b: int = 2, c: str = "titi" ) -> Tuple[int, float, str]: """This function illustrates Typehint 'Tuple' (immutable) Args: a: the path of the file to wrap b: instance to wrap c: toto Usage: >>> e = general_function_that_returns_a_tuple_of_3_elem(1, 2, 'toto') >>> e (1, 2, 'toto toto') You can name parameters if you don't pass all of them (here we don't pass optional parameter 'b') : >>> e = general_function_that_returns_a_tuple_of_3_elem(c='toto', a=1) >>> e (1, 2, 'toto toto') >>> f,g,h = general_function_that_returns_a_tuple_of_3_elem(c='toto', a=1, b=2) >>> f,g,h (1, 2, 'toto toto') """ return (a, b, c + " toto") # - Typehint : Sequence = List (mutable) or Tuple (immutable) def square(elems: Sequence[float]) -> List[float]: """This function illustrates Typehint 'Sequence' Takes a Sequence (either List of Tuple) as input, and send a List as output Usage: >>> square( [1,2,3] ) [1, 4, 9] >>> square( (1,2,3) ) [1, 4, 9] """ return [x ** 2 for x in elems] # - Typehint : TypeAlias Card = Tuple[str, str] Deck = List[Card] SUITS = "♠ ♡ ♢ ♣".split() RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() def create_deck_without_alias(shuffle: bool = False) -> List[Tuple[str, str]]: """This function (and the next one) illustrates Typehint 'TypeAlias' Create a new deck of 52 cards """ deck = [(s, r) for r in RANKS for s in SUITS] if shuffle: random.shuffle(deck) return deck def create_deck_with_alias(shuffle: bool = False) -> Deck: """This function (and the previous one) illustrates Typehint 'TypeAlias' Create a new deck of 52 cards Card = Tuple[str, str] Deck = List[Card] Return Deck """ deck = [(s, r) for r in RANKS for s in SUITS] if shuffle: random.shuffle(deck) return deck # - Typehint : Generics avec Sequence, Any, TypeVar def choose_from_list_of_Any_returns_a_Any(items: Sequence[Any]) -> Any: """This function illustrates Typehint 'Generics' Option1 (BAD) : avoid using 'Any' because too general """ return random.choice(items) T = TypeVar("T") def choose_from_list_of_a_specific_type_returns_same_type(items: Sequence[T]) -> T: """This function illustrates Typehint 'Generics' Option2 (BETTER) : prefer 'TypeVar' instead of 'Any' T = TypeVar("T") """ return random.choice(items) T_str_flt = TypeVar("T_str_flt", str, float) def choose_from_list_of_a_specific_constrained_type_returns_same_type( items: Sequence[T_str_flt], ) -> T_str_flt: """This function illustrates Typehint 'Generics' Option3 (still BETTER) : use a 'constrained TypeVar' T_str_flt = TypeVar("T_str_flt", str, float) (you could name 'T_str_flt' as you want, for example, 'Choosable'...) => the function accepts only sequence of str or float: - if str: return str - if float: return float Usage: choose(["Guido", "Jukka", "Ivan"]) => str, OK choose([1, 2, 3]) => float, OK (car int subtype of float) choose([True, 42, 3.14]) => float, OK (car bool subtype of int which is subtype of float) choose(["Python", 3, 7]) => object, KO (rejected) """ return random.choice(items) # - Typehint : Callable def create_greeting(congrat: str, name: str, nb: int) -> str: return f"{congrat} {name} {nb}" def do_twice( func: Callable[[str, str, int], str], arg1: str, arg2: str, arg3: int ) -> None: """This function illustrates Typehint 'Callable' Usage: >>> do_twice(create_greeting, "Hello", "Jekyll", 1) Hello Jekyll 1 Hello Jekyll 1 """ print(func(arg1, arg2, arg3)) print(func(arg1, arg2, arg3)) # - Typehint : typing.Literal (new in 3.8) def validate_simple(data: Any) -> Literal[True]: """This function illustrates Typehint 'Literal' (new in 3.8) => should always return True """ pass MODE = Literal['r', 'rb', 'w', 'wb'] def open_helper(file: str, mode: MODE) -> str: """This function illustrates Typehint 'Literal' (new in 3.8) (Type alias :) MODE = Literal['r', 'rb', 'w', 'wb'] Usage: OK : >>> open_helper('/some/path', 'r') Error : >>> open_helper('/other/path', 'typo') """ pass # ''' # ******************************* # CUSTOM EXCEPTIONS CLASSES # ******************************* # See https://docs.python.org/3/tutorial/errors.html # ''' class DCCNotFoundException(Exception): """Raised when a specific DCC is not available""" pass class UnknownGenericCmdArgException(Exception): """Raised when a GENERIC cmd argument is not recognized by the controller (no native cmd available for the generic cmd)""" def __init__(self, name: str, arg: str): self.name = name self.arg = arg def __str__(self): return f"The argument '{self.arg}' does not exist for generic cmd {self.name}" class UnknownNativeCmdException(Exception): """Raised when a NATIVE command name is not recognized by the controller""" def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) class UnimplementedGenericCmdException(Exception): """Raised when a GENERIC cmd has no implementation in the controller (no native cmd available for the generic cmd)""" def __str__(self): return f"({type(self).__name__}): Device Generic command has no implementation in the controller" class MyImprovedException(Exception): """Improved Exeption class with predefined list of standard error messages, and optional specific message Usage: >>> e = MyImprovedException(MyImprovedException.ERROR_BAD_PARAM) >>> e.error_msg 'Bad Parameter' >>> e MyImprovedException('Bad Parameter') >>> print(e) (MyImprovedException): Bad Parameter >>> e = MyImprovedException(MyImprovedException.ERROR_BAD_PARAM, "my specific message added") >>> e MyImprovedException('Bad Parameter', 'my specific message added') >>> print(e) (MyImprovedException): Bad Parameter ; Specific message: my specific message added """ # List of standard error messages ERROR_UNDEFINED_PARAM = "Parameter not defined" ERROR_BAD_PARAM = "Bad Parameter" ERROR_MISSING_PARAM = "a Parameter is missing" ERROR_TOO_MANY_PARAM = "Too many Parameters" def __init__(self, error_msg: str, specific_msg: str = None): # super().__init__(error_msg) self.error_msg = error_msg self.specific_msg = specific_msg def __str__(self): #msg = f"({type(self).__name__}): {self.error_msg}" msg = f"({self.__class__.__name__}): {self.error_msg}" if self.specific_msg: msg += f" ; Specific message: {self.specific_msg}" return msg class MyOwnDerivedException(MyImprovedException): """ Usage: >>> try: ... print("doing something dangerous...") ... raise MyOwnDerivedException(MyOwnDerivedException.ERROR_MISSING_PARAM) ... except MyOwnDerivedException as e: ... print(e) doing something dangerous... (MyOwnDerivedException): a Parameter is missing >>> try: ... print("doing something dangerous...") ... raise MyOwnDerivedException(MyOwnDerivedException.ERROR_NEW_CASE2, 'my own special message') ... except MyImprovedException as e: # we can use superclass also ... print(e) doing something dangerous... (MyOwnDerivedException): Nouveau cas d'erreur 2 ; Specific message: my own special message General example : >>> try: ... # do something ... pass ... ... except ValueError: ... # handle ValueError exception ... pass ... ... except (TypeError, ZeroDivisionError): ... # handle multiple exceptions ... # TypeError and ZeroDivisionError ... pass ... ... except: ... # handle all other exceptions ... pass """ # Add new specific error cases for this exception type: ERROR_NEW_CASE1 = "Nouveau cas d'erreur 1" ERROR_NEW_CASE2 = "Nouveau cas d'erreur 2" ''' try: print("doing something dangerous...") raise MyOwnDerivedException(MyOwnDerivedException.ERROR_MISSING_PARAM) except MyOwnDerivedException as e: print(e) ''' # ''' # ================================================================= # GENERAL CLASSES # ================================================================= # ''' class MySuperClass1: pass class MySuperClass2: pass # ''' # ================================================================= # - CLASS MySimpleClass # ================================================================= # ''' class MySimpleClass(MySuperClass1, MySuperClass2): """a Class with multi-inheritance blabla blabla """ # # Class attributes # names: List[str] = ["Guido", "Jukka", "Ivan"] """ List is mutable""" version: Tuple[int, int, int] = (3, 7, 1) """ Tuple is IMMutable""" options: Dict[str, bool] = {"centered": False, "capitalize": True} """ Dict (is mutable) """ my_attr1: dict = {} current_file = None # # Class methods # def __init__(self, a: int, b: float) -> None: """ La methode __init__ doit toujours retourner "None" Args: a: blabla """ c = 1 d = 2 def __str__(self) -> str: """ La methode __str__ doit toujours retourner "str" """ return "toto" def my_method2(self, a: int, b: float) -> None: """Method that returns nothing""" a = 1 b = 2 # ''' # ================================================================= # - CLASS Person # ================================================================= # ''' class Person: """Class to create a person, in several ways (several Factory methods) => Illustrate difference btw static and class methods Usage: 1) Classic Constructor : >>> person1 = Person('Alfredo', 21) 2) Class method (Factory) : >>> person2 = Person.fromBirthYear('Peter', 2000) >>> person2.age 22 3) Another class method (Factory) : >>> person3 = Person.twin('John', person2) >>> person3.age == person2.age True >>> person3.name == person2.name False 4) Static method (does not need access to the class attributes or methods) : >>> Person.isAdult(22) True """ def __init__(self, name: str, age: int) -> None: self.name = name self.age = age @classmethod def fromBirthYear(cls, name: str, year: int) -> Person: """A class method to create a Person object by birth year NB : return type 'Person' is possible because of: 'from __future__ import annotations' """ return cls(name, date.today().year - year) @classmethod def twin(cls, name: str, p: Person) -> Person: """A class method to create a Person object from another""" return cls(name, p.age) @staticmethod def isAdult(age: int): """A static method to check if a Person is adult or not""" return age > 18 # ''' # ================================================================= # - CLASS Employee # ================================================================= # ''' # class typing.NamedTuple : Typed version of collections.namedtuple() class Employee(typing.NamedTuple): """Illustrates usage of collections.namedtuple & typing.NamedTuple See https://towardsdatascience.com/what-are-named-tuples-in-python-59dc7bd15680 See https://www.netjstech.com/2020/01/named-tuple-python.html Usage: 1) Here we use typing.NamedTuple : >>> andrew = Employee('Andrew', 'Brown', ['Develoer', 'Manager'], 'US') >>> print(andrew) Employee(first_name='Andrew', last_name='Brown', jobs=['Develoer', 'Manager'], country='US') >>> alice = Employee(first_name='Alice', last_name='Stevenson', jobs=['Product Owner']) >>> print(alice) Employee(first_name='Alice', last_name='Stevenson', jobs=['Product Owner'], country='France') >>> alice.last_name 'Stevenson' >>> alice[1] 'Stevenson' >>> for attr in alice: print(attr) Alice Stevenson ['Product Owner'] France 2) Here we define the same Employee as above, but using collections.namedtuple : >>> from collections import namedtuple >>> Employee = namedtuple('Employee', 'first_name last_name jobs country') or : >>> Employee = namedtuple('Employee', ['first_name', 'last_name', 'jobs', 'country']) """ first_name: str last_name: str jobs: list country: str = "France" # ''' # ================================================================= # - CLASS Point2D # ================================================================= # ''' # typing.TypedDict : Special construct to add type hints to a dictionary. At runtime it is a plain dict class Point2D(typing.TypedDict): """Class to illustrate typing.TypedDict (Special construct to add type hints to a dictionary. At runtime it is a plain dict) See: https://adamj.eu/tech/2021/05/10/python-type-hints-how-to-use-typeddict/ Usage: a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK b: Point2D = {'x': 1, 'y': 2} # KO (missing label) c: Point2D = {'z': 3, 'label': 'bad'} # KO (z not defined) d: Point2D = {} # KO (missing x, y, label) Definition (other possibilities): Point2D = TypedDict('Point2D', x=int, y=int, label=str) Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) """ x: int y: int label: str def get_point() -> Point2D: return { 'x': 1, 'y': 2, 'label': 'good' } # ''' # ================================================================= # - CLASS Position # ================================================================= # ''' # @dataclass : shortcut for class definition @dataclass class Position: """@dataclass Class to illustrate Data Classes (@dataclass) See: https://realpython.com/python-data-classes A DataClass is a shortcut to define a "data structure" without method It is much like a NamedTuple, but "mutable", and with more features. Defines automatically a lot of things for you : - __init__() (with self.x = x, self.y = y, ...) - __repr()__ - __eq()__ - order, sort, immutable or not (frozen=True), ... NB: on peut quand même ajouter des méthodes à une dataclass car c’est une classe normale... Definition alternatives : >>> from dataclasses import make_dataclass >>> Position = make_dataclass('Position', ['name', 'lat', 'lon']) Usage: >>> pos = Position('Oslo', 10.8, 59.9) >>> pos Position(name='Oslo', lat=10.8, lon=59.9) >>> pos.lon 59.9 """ name: str lon: float lat: float = 0.0 # with default value @dataclass class PlayingCard: """@dataclass Class to illustrate Data Classes (@dataclass) """ rank: str suit: str @dataclass class Deck: """@dataclass Class to illustrate Data Classes (@dataclass) Usage: >>> queen_of_hearts = PlayingCard('Q', 'Hearts') >>> ace_of_spades = PlayingCard('A', 'Spades') >>> two_cards = Deck([queen_of_hearts, ace_of_spades]) """ cards: List[PlayingCard] @dataclass class Cmd: """@dataclass PyROS example class to illustrate Data Classes (@dataclass, and using 'field') Usage: >>> c = Cmd('get_timezone') >>> c = Cmd('do_init','do_init'), """ generic_name: str = 'generic name' native_name: str = '' desc: str = 'Description' # equivalent to "= {}" which is not allowed params: Dict[str, str] = field(default_factory=dict) final_simul_response: str = 'simulator response' final_device_responses: Dict[str, str] = field(default_factory=dict) immediate_responses: Dict[str, str] = field(default_factory=dict) errors: Dict[str, str] = field(default_factory=dict) # ''' # ================================================================= # Main function (definition) # ================================================================= # ''' def main() -> None: """Comment on Main function definition""" a = 1 b = 2 c = a + b e = general_function_that_returns_a_tuple_of_3_elem(c="toto", a=1, b=2) # print(e) import doctest doctest.testmod() # """ # ================================================================= # Main function (execution) # ================================================================= # """ if __name__ == "__main__": """Comment on Main function execution""" main()