From c93672b3da51093db869fba31b9cfadc6f4e0ad9 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Thu, 30 Dec 2021 19:16:58 +0100 Subject: [PATCH 01/32] Add logging guidelines. --- doc/source/logging.rst | 249 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 doc/source/logging.rst diff --git a/doc/source/logging.rst b/doc/source/logging.rst new file mode 100644 index 000000000..0cf522fa8 --- /dev/null +++ b/doc/source/logging.rst @@ -0,0 +1,249 @@ + .. _ref_guide_logging: + +Logging Guideline +################### + +This page describes the general framework for logging in PyAnsys package and its libraries. + + +Logging in PyAnsys +=================== + +The logging capabilities in PyAnsys are built upon the `logging `_ library. +It does *NOT* intend to replace this library, rather provide a standardized way to interact between the built-in ``logging`` library and ``PyAnsys`` module. +There are two main loggers in PyAnsys, the *Global logger* and *Instance logger*. +These loggers are customized classes that wraps the ``Logger`` class from ``logging`` module and add specific features to it. + + +.. figure:: images/Guidelines_chart.png + :align: center + :alt: Logging in PyAnsys + :figclass: align-center + + **Figure 1: Loggers structure in PyAnsys** + + +Global logger +~~~~~~~~~~~~~~~~~ + +There is a global logger named ``pymapdl_global`` which is created when importing ``ansys.mapdl.core`` (``ansys.mapdl.core.__init__``). +This logger is recommended for most scenarios, especially when the library ``pool`` is not involved, since it does not track the instances, rather the whole library. +If you intend to log the initialization of a library or module, you should use this logger. +If you want to use this global logger, you must import it at the top of your script or module: + +.. code:: python + + from ansys.mapdl.core import LOG + +You could also rename it to avoid conflicts with other loggers (if any): + +.. code:: python + + from ansys.mapdl.core import LOG as logger + + +It should be noticed that the default logging level of ``LOG`` is ``ERROR`` (``logging.ERROR``). +To change this and output different error level messages you can use the next approach: + +.. code:: python + + LOG.logger.setLevel('DEBUG') + LOG.file_handler.setLevel('DEBUG') # If present. + LOG.stdout_handler.setLevel('DEBUG') # If present. + + +Alternatively, you can do: + +.. code:: python + + LOG.setLevel('DEBUG') + + +This way ensures all the handlers are set to the desired log level. + +By default, this logger does not log to a file. If you wish to do so, you can add a file handler using: + +.. code:: python + + import os + file_path = os.path.join(os.getcwd(), 'pymapdl.log') + LOG.log_to_file(file_path) + + +This sets the logger to be redirected also to that file, in addition of the standard output. +If you wish to change the characteristics of this global logger from the beginning of the execution, +you must edit the file ``__init__`` in the directory ``ansys.mapdl.core``. + +To log using this logger, just call the desired method as a normal logger. + +.. code:: python + + >>> import logging + >>> from ansys.mapdl.core.logging import Logger + >>> LOG = Logger(level=logging.DEBUG, to_file=False, to_stdout=True) + >>> LOG.debug('This is LOG debug message.') + + | Level | Instance | Module | Function | Message + |----------|-----------------|------------------|----------------------|-------------------------------------------------------- + | DEBUG | | __init__ | | This is LOG debug message. + + + +Instance logger +~~~~~~~~~~~~~~~~~ +Every time that the class ``_MapdlCore`` is instantiated, a logger is created. +This logger is recommended when using the ``pool`` library or when using multiple instances of ``Mapdl``. +The main feature of this logger is that it tracks each instance and it includes its name when logging. +The name of the instances are unique. +For example in case of using the ``gRPC`` ``Mapdl`` version, its name includes the IP and port of the correspondent instance, making unique its logger. + + +The instance loggers can be accessed in two places: + +* ``_MapdlCore._log``. For backward compatibility. +* ``LOG._instances``. This field is a ``dict`` where the key is the name of the created logger. + +These instance loggers inherit from the ``pymapdl_global`` output handlers and logging level unless otherwise specified. +The way this logger works is very similar to the global logger. +You can add a file handler if you wish using the method ``log_to_file`` or change the log level using ``setLevel`` method. + +You can use this logger like this: + +.. code:: python + + >>> from ansys.mapdl.core import launch_mapdl + >>> mapdl = launch_mapdl() + >>> mapdl._log.info('This is an useful message') + + | Level | Instance | Module | Function | Message + |----------|-----------------|------------------|----------------------|-------------------------------------------------------- + | INFO | 127.0.0.1:50052 | test | | This is an useful message + + + +Other loggers +~~~~~~~~~~~~~~~~~ +You can create your own loggers using python ``logging`` library as you would do in any other script. +There shall be no conflicts between these. + + +For instance, if an ANSYS product is using a custom logger encapsulated inside the product itself, you might benefit from exposing it through the standard python tools. +It is recommended to use the standard library as much as possible. It will benefit every contributor to your project by exposing common tools that are widely spread. Each developer will be able to operate quickly and autonomously. +Your project will take advantage of the entire set of features exposed in the standard logger and all the upcoming improvements. + +Create a custom log handler to catch each product message and redirect them on another logger: +============================================================================================== + +Context: +~~~~~~~~~ + +AEDT product has its own internal logger called the message manager made of 3 main destinations: + + * *Global*: for the entire Project manager + * *Project*: related to the project + * *Design*: related to the design (most specific destination of each 3 loggers.) + +The message manager is not using the standard python logging module and this might be a problem later when exporting messages and data from each ANSYS product to a common tool. In most of the cases, it is easier to work with the standard python module to extract data. +In order to overcome this limitation, the existing message manager is wrapped into a logger based on the standard python `logging `_ module. + + +.. figure:: images/log_flow.png + :align: center + :alt: Loggers message passing flow. + :figclass: align-center + + **Figure 1: Loggers message passing flow.** + + +To do so, we created a class called LogHandler based on logging.Handler. +The initializer of this class will require the message manager to be passed as an argument in order to link the standard logging service with the ANSYS internal message manager. + +.. code:: python + + class LogHandler(logging.Handler): + + def __init__(self, internal_app_messenger, log_destination, level=logging.INFO): + logging.Handler.__init__(self, level) + # destination is used if your internal message manager + # is made of several different logs. Otherwise it is not relevant. + self.destination = log_destination + self.messenger = internal_app_messenger + + def emit(self, record): + pass + + +The purpose of this class is to send log messages in AEDT logging stream. +One of the mandatory actions is to overwrite the ``emit`` function. This method operates as a proxy. It will dispatch all the log message toward the message manager. +Based on the record level, the message is sent to the appropriate log level (debug, info, error...) into the message manager to fit the level provided by the ANSYS product. +As a reminder the record is an object containing all kind of information related to the event logged. + +This custom handler is used into the new logger instance (the one based on the standard library). +A good practice before to add a handler on any logger is to verify if any appropriate handler is already available in order to avoid any conflict, message duplication... + +App Filter +~~~~~~~~~~ +In case you need to modify the content of some messages you can apply filters. This can be useful to harmonize the message rendering especially when you write in an external file. To do so you can create a class based on the logging.Filter. +You must implement the ``filter`` method. It will contain all the modified content send to the stream. + +.. code:: python + + class AppFilter(logging.Filter): + + def __init__(self, destination="Global", extra=""): + self._destination = destination + self._extra = extra + + def filter(self, record): + """Modify the record sent to the stream."""" + + record.destination = self._destination + + # This will avoid the extra '::' for Global that does not have any extra info. + if not self._extra: + record.extra = self._extra + else: + record.extra = self._extra + ":" + return True + +Avoid printing to the console +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A common habit while prototyping a new feature is to print message into the command line executable. +Instead of using the common ``Print()`` method, it is advised to use a ``StreamHandler`` and redirect its content. +Indeed that will allow to filter messages based on their level and apply properly the formatter. +To do so, a boolean argument can be added in the initializer of the ``Logger`` class. This argument specifies how to handle the stream. + +.. code:: python + + class CustomLogger(object): + + def __init__(self, messenger, level=logging.DEBUG, to_stdout=False): + + if to_stdout: + self._std_out_handler = logging.StreamHandler() + self._std_out_handler.setLevel(level) + self._std_out_handler.setFormatter(FORMATTER) + self.global_logger.addHandler(self._std_out_handler) + + +Formatting +~~~~~~ +Even if the current practice recommends using the f-string to format your strings, when it comes to logging, the former %-formatting suits the need. +This way the string is not constantly interpolated. It is deferred and evaluated only when the message is emitted. + +.. code:: python + + logger.info("Project %s has been opened.", project.GetName()) + + +Enable/Disable handlers +~~~~~~~~~~~~~~~~~~~~~~~ +Sometimes the customer might want to disable specific handlers such as a file handler in which log messages are written. +If so, the existing handler must be properly closed and removed. Otherwise the file access might be denied later when you try to write new log content. + +.. code:: python + + for handler in design_logger.handlers: + if isinstance(handler, logging.FileHandler): + handler.close() + design_logger.removeHandler(handler) From 61611660747a065ea8ec20be9865427c5f6e9211 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Thu, 30 Dec 2021 20:29:50 +0100 Subject: [PATCH 02/32] Add logging to the Guidelines section. --- doc/source/guidelines/index.rst | 4 +++- doc/source/{ => guidelines}/logging.rst | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename doc/source/{ => guidelines}/logging.rst (100%) diff --git a/doc/source/guidelines/index.rst b/doc/source/guidelines/index.rst index 89d0a8f80..b4f37395f 100644 --- a/doc/source/guidelines/index.rst +++ b/doc/source/guidelines/index.rst @@ -16,5 +16,7 @@ functionalities such as logging, data transfer... dev_practices app_interface_abstraction - service_abstraction data_transfer_and_representation + logging + service_abstraction + diff --git a/doc/source/logging.rst b/doc/source/guidelines/logging.rst similarity index 100% rename from doc/source/logging.rst rename to doc/source/guidelines/logging.rst From 197f8d8fbb5df163cf0406a7aa73dbba61fd210d Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Thu, 30 Dec 2021 20:31:33 +0100 Subject: [PATCH 03/32] Remove new line. --- doc/source/guidelines/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/guidelines/index.rst b/doc/source/guidelines/index.rst index b4f37395f..291ad183f 100644 --- a/doc/source/guidelines/index.rst +++ b/doc/source/guidelines/index.rst @@ -19,4 +19,3 @@ functionalities such as logging, data transfer... data_transfer_and_representation logging service_abstraction - From 5a266c1591cae2e8d0c07ac6983c677b298a125f Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Thu, 30 Dec 2021 20:34:00 +0100 Subject: [PATCH 04/32] Add images for the logging guideline. --- .../guidelines/images/Guidelines_chart.png | Bin 0 -> 23817 bytes doc/source/guidelines/images/log_flow.png | Bin 0 -> 11516 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/source/guidelines/images/Guidelines_chart.png create mode 100644 doc/source/guidelines/images/log_flow.png diff --git a/doc/source/guidelines/images/Guidelines_chart.png b/doc/source/guidelines/images/Guidelines_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..5b57653d9653f557b34fff0e833f5b3178e0674b GIT binary patch literal 23817 zcmeGEc~p~G(>@9Zc^V#DkXE||Wa@4iS{x9W=XL@Sfp&n%EHV=y3^FDVY}#2sp#f!z zihvRz3NjOd4T7Kn0YiX5f{YRf5keA@kmT%uJwEUI``&M@v(8#)o%atG#_apvwX15^ zuDY(;-1y1CM)u2HUqT=dS=$rG&OjhvfPZd!_wP;M*N%IOLuR6Vs!|9Ey>_7FwdN2<6%yn;} z$L{#h)RLp$Z+dB)YrDz#xo%p@Eu|yS!`gJQr1}tiuOsn|A%m4lGzdHC|KltCh z@c+_ya8o(iQJJO^*H||zjj<3#+h(ZSYS#rHN)UA7CgE(Y6680A5VU05f zk0OmIO=FdoDrf0K)P_l97X8xY^>3nekeJv&DC%mrgRW%wDSTJsNiaS`DkeMtmC
aPDLay8qA%#s}fKtXp7k4#N?88jPKMF$;HKP?IKDYAOi&b04f# z!~!cc_l1^2^U~WnBea)EeUu+{H?Dws3Qz36SUXP^Jk^%}LZ-C|pIP|`2xT2Zd&N+V zXDt;;$22!ia$-dE=-$NHn*pL_ueB8&QL!eB)fCIX(wjw8-0~FaCVXM82+Q$Ae9tYk z+v4i^r9yHhD-*2b^;SkR|NWAYCvr6^eT?U2fgI2z)Y>F*0?_;(ylH%#Bcme6nHpHf z66EZ}G%F;7m9508BrU?U(&<#;th}aAo2?2F2>=lOAD7>YwiCCp z))%=FaR(HWYd%iDg6spnN0e!e>VlLX*ZDkyKcvu3>cakncwLBHhI){JYF5k250c}{Xd^}zeHw?Ma~`1L-{dvWEUp3=+p~y-_#{d4|G-R z+%I0WUe}*GInaoyTn$8pE6)iSW_&%USInhOl`ew3`hO1e8eX##yo``hGZjf9R2%m< zTs!_6XsQ3yp+9watI`D0`7uslkV8sb%Sg|UT{IEQQ`om=*VZ=mw2Ix*KjW{W5+sZ} zUQdk#@^NklT0Z?O+!?TMoV-s7xOuMapOy;mt1yAs*u;+ZjV_EaOy{3B;qycEsABge zsQ+n$`i~uo$wiXnFs$fZ*y?Z5P)((`M*q1EUFq?UM`uwF@MCdDXCz}O3bDol9T_VW zl@I~#x?-!O3+bI&>P$g*RZjy=ZkO|4*<_ocpZHgCQmr6fz%r~l@{1tmHLH8>ZZ)jE z=rk@89h4Zt0d=K`RU(aPC?uk6fVm?@Sn+J5i(ifQ?oC-tamh zL#g)GTqQShzE&Y6a|sSC;7^({R!LXC?;pTg_3^FnU(l6pYNK<)Y-dErZohnPAGy~y zqvY*heP&5|d`7Po>ao-}<3t#avoG@MRc;GKHQmkF6T1S*Z@vMQdD8VJL;OA-9llaa zP!nF@JK7qIMa|206&%z3rcSi^L88@jg0bkqx3>MOq3w{hNwXyGN>`hpx=POhZQ4ke zLwz?KLZI=7S*>tQ=zhfKeJ4M*Dj6R2=H5y!^wnDz57lbFw;j6giM?1$U_f0@L-g07 zRtWbTnL9b2FHEGG6`Us>CZ2ns2-B?07x=iIUL&}10wheRSjt{eA^5|oAW6}k1R~0E zxhViw!+2?Y*o~pg@83;QvwAWATBs#QZ_sH79@g844ZtszlQW<0yFYp{Jhe|>>b*B! zL&?XNxrcBW{ex7)jA2*iTPK$K{k84O*Cpyz!`fQsjB>js;c3He_~p5&kslWEC{~Gm z?AXfi)JP*QCuve|Ycu)@H)L&-(8b@je|hjFL|O1@hLdm3>wccnYmqQN%(_y93ELjK z-RD#HTxKtFKEpSqxMw4hjXzR~f`qkvGY~&psQ1$Bpg}-WsXSbWMON)r&bXits9;%27ct(bB$G?n?(X7q#1AIce2 zPWA~5alq?_rLJlWZ8NZG>UB?ai3~k;iBNi8TIj!UU21EqZV6i_H-VRGZ6yR7oZ)2@ z8D4?UT#C!Ul;t{#(o|vi@Ubjnw)fDER~hORb&JyD>>tfOaX%;=!_cKYl_bNX`=Miw zOd&>=me<+26qnX4EUM?`q7%j|XsH4BK1c22Icxs#QyET65i*ek*j|!XRD7hI&meMJ z^s@H!4_9fz=n5)BrIZa(*7FwO19Sz}lUBmjK@HNN0d-<;gSL2w3ZwH3J&+&1=&G?L z=$KsnFy(5Sp^3?psBe7CbE2@HnT8ywK(UPCGYre5#%Jtl0kl3)(RW~?~md=)G<%}jA6(E;Sl~4m$CaKJK zp@Mm9OG$T;Ul9lpS`FI4JwtS@`-GM{VOmyy+cL<42B_6DI}}cej}x(WoS3nJ zWJx#HpmWwX$*E_#lAmVeP|t);o#be9K+t1e1L5KeD*&k4`gQRI<&K{E{TRYfhF()CfIUf4UNsTgR3qIuWl#{Z$2 zw+@0mWyQN*T+~+)m0SXU#2ci@QR+jcyX{mRdj8HjRvg9!vX&z-uVhI-%Z&3Aey zK0A-AMz1th6$i z8a(qk9dN%ykcg|&vRgL<&~7Qp1{NfDhcLI4t>c;J-sBGg>0&eYT(H8DT|@L)OJQK| z(K?NkSo5-aa_6Ej5_z0pCVIzJQ&>H@JcHj{)AHE?svV+t)x0`D2gxAA)q=x!s z^r)^E2~(*p%o&vyL`7_6p>WAY7eYei2L2Lvu+8%n1t}Fb?&S(nm!dop#2vS``U#UR zgA_y|4ph`)b-YLrWhEnx*CbWC zARv~&@n_&&9Y_Q?^9XtRd0=8zY-GEFIS=|YPmi4o|5?Mf-&!9lVVoeKsM4;bA7Gg8}k7jM?T{S)Lh4!9y>BY$Dwy z>H)ppB#EZ|Inlfo#*?V$l52NU@WCt8iqv7OaK~Zq{JM%@!w^Ust?xy@a96c#XVsqWHkT9LX@eO6@-&zKd5?|m+^G9s9%B{C&*JW{KH>8@AU0ktZ?JE@|8`8W;$q--jd;O zAJG-#9@N*($mSiLd}ihdYEG_FE${%{cZSk)28kcnBK0)p^n?~Q@jYZoWas_XnxIJ0C_O0Ay_ z=9htN?8Y^Dj_5u{2PO=fI5?rB+G(M$Mn4F$oCa=#x^3>EzvjUg4>p0EzevYDc6tTg zaOsUaHub|n%{qlmR+og*;zYataSNFj^-7v$FI5YScd9LUC%<-WD_F@HQ{ z9HyXlj(W4gi@UNiuVH)UFS-6cd#6*)#2=Nc&7ZoFka-vdme`kbef~_VpXob!)h~7n zjm;=(jL?>^5{9r6AWDPk?T;EPsLV2o7CL1-KK|y< z6g9hqR+(q%Ci}u0r02#H(5Upq`bp?6c<48oQB{}KZJB>93rn;$DKlheyeKgMH5Y|G zC49>emj&`2PJ&cxp&}>~XR`h*TK3tKW9x*2xhUKEJyJ8iN3zv}w*937DpvejFMK&5 zN&03V2SjO({B3pHTYo9f_7Tc~F`o)E!k2#ue~Xj%ihG%3$+0cMst4Wui>HVYjQ^*K z(f|MJVD?OP9sbWcd@L9;c;xqr+H7@!jJ>zSwAYEh)c^NAdBQ${Rnkf}>fWiqk#`O_t~(syIr|^K+OtuC7X-~3rL_o) zW?T64UB3Xlq7#KoVoZIY*ir3BM_ltas{Awf9Xcq+>v>0zpIha<4H|in#y~uYje}Qx z*?%!>qw=SO&5m_$UGt@;qs;CPyx2xZOkJfsoy5tDOg+8`diWW!NR6Q)mOr&X3wZ@< zlr_1G%l3K;#7?0PW90?~lU_na1@(2?Nl1RGgqPa=004t+bYaiax)`;1!Hrow2d*lj z@%h;6!UA;Pxv>?_BO7=;BZ?y&=`t#G5fG_~a>dm6Ac;5N4bP0exdT8f)LyK4%<_0; z<~5FLVkr4JN!5(atGz;BEkroBe3I_;M3^@q?a2DW3td-TZ#1U)62TID|GmE12*?8P<}$^g3b(3uX1G9^ya!vP$L z(|0A#=f-0#Dd@+`XWl}T*>FdBCoo>o7n1Gob_&0@l<#xS^W?7Nw&wy2!}nf!C2-Gd zQ^{xg7|&JbY%H=8pp4hD1~59;lDNW4#o~?fSTVVH4l#$oVDA$aJ+RxSWdX?S>pvTc zv#}gDi+(IZLyjsWOF+&?N?o{ME_DF{$2me`En>jS0+ zFK=EA*y|`l;m+C=E5`By#cthI=WKPNGNS~hA>@n?;6g=B@l7VLc&NTVt`}Luc}-mRjozpKE&O{%S}|E;zmJAYD}ik} zWENd+WikEavBc`@77ExbTL8M&*ag`aq_{T}k_HURcvmTu$tt)gHGVHCGa3ND9{!N* z7q`CwiJHpju-I2RAV}UXvq|90hypm$oAb{lKlL<0sb+i3{A5~_7|@Kz4FT(qm_clG zs8q4rwL*|QW#I4?xloQ?pKX&%-@y4ItVR1{V zc(b(u(WQyj$-OpS3>^E|vTTli=0cPIm=vRB87w`&9IbBaa_J&xY_Ar@c;)>pJ8=>} z5;dzysA@`Lr(P2h=XN6!;J6n&%O@4^md7`$86!i67dad8@eS#QhnHD2QUs$m9kgKe zEsVr+_zw7*sOfi$Q+YZ;6AMj^K^;xlb==go$^jqFnVZBiL@%uzd*bw&p4=HLSCb8> zdHadnZ~2u6@}?Izfa&b=NgZmGFG)>()^y~-)-XN?qji_E!!Fm9UzwIS{QIaf06Q;_ zvq{2)sK`e}sWcc0H7!P+|E=!wZ`=9q77X9NhQA)BAN$c(7U8N^pFS2gC23mp^<9l# zCT?KQ>my+Y2>!n>>;00v@J(M1zA%3x@TeL4w?WE7kFFypT%l}_s361N*He;)|72qm zlW1V_s_$w{$gCQBEAHODUmi#MQ4MnZtN)mGb&Fei^Cy1So;1T3#t{sz&x!s^!?A?o z;f0po1<7BS_cxGKN+yeO+5Km$Rg#NvD0bmMr>7#*7h`(btPuel zeQeQg9YO4VT67JKf10?-HSy8vdtIqItOxV5thq)-ZIC6jnew~%7*KHwdK+CCVZODJ z;V&99^C!0EDgH5mT5_*B$(3*igH;VFFgDaFcX&TBBUxA8(PylyVmvXZ70)Wr$EpZg zrNVy78qJe9@A}r?0MhBm`4=Be%xkXTI{G^iLou*P)r_$d{H^o2;a1V@+*k$c^eTFu zQnJynfY^BK7kpHD-1nY4j{P>sU7#O=JB)bq#L`|l2FRX0Wr@*LP0mCBW!xH;ksgnx zqcS{>iAZ~|!hcHdW(n$I1BsFfh7c&?A4`8*UAbP3?-Or^%+zb9~*h|mQ+B80$O6Cxy;0|*6~fQ)`QcD+f8`L z=Z&={@7OjI9R*l!>($UZ`O?^Us~sR$D1if)(C-ljyp@Psv8uB_2&uE0ygq^$nWC~v zx_*D>$VUldR|~RPl;O_V>s^ZHBR9E<0ul{fJPPx5oMVlDO3baFkyNTZynLsFyNLu` zB)8?hT9dTq&%bvKrHwwZqB211e6#;29TZ@w1%5b+M)TrkrnEM+O1OK zPfSHyx-z$SZ4NwV)kGexiU+>fak=-}IdpO3hkpUA(>8L(bVk0ua@}K)khH;3(J`Xs z!=$v>A$+dE?1upxudqeyjMBU0rcU3V?Ja%2mJ1rUd{$-wfqIcnu|ZJ=LPS+X?unzP zO1qq@=}m4ZjOe~$DDN}j8R59ZsUv9+e!;3*A2E?YTSThcfB(E+WDs=%2 zPpwSJd`*+@Zx%N6K=#>huzw(OenC`D#x_nA3DSFz`5`8aMP`d}B7RS0SQEER?eOsh_ZC5ptAa6iM^p}pNCOmZKHSn^exgZ z@q>+TZGAy9R6JMB$h8xjIsCAJ^!UAD0N;oSql{FWb5zIM?{rO+{+rPnlAnBHQ4J!s z`S`u!N_LLwKTra*T6$jJ4hex|zxQ+azNuHdqC^tlM!|8mMO!g>GUFbCFS1~}dB1u` z#=j&RxiBEgb!EbYaKWO>(m_BxFj*w`B$7Q^EHaZb=04^at@WWfs&^QmJw%<^MK4od z2nDEJ4L`Lzf(Zz*@yllLQADU2xQ)6+e*k3D zsJZo^u;gg_w{E^CM=UjmTkZQatu%u z=`U;WbKe4ZCVqu_CT*LMCR;d|a=SV$BfBxo;d_p77~!CX@WTO7aEU9cD2upmykRrN z(I`UfJ_}7WQ*!L_4g1spvJK6wNP1Y;kl3^zEjm{dF~rMqgvzG@u61e<`CI%iaVJ;> za^)svXW>IX`Zo=LH?2vS(cai+Iu#DIBmb4e&kVY zAA^`@$**keVSC&a2O;;L=n}@kmftDrdE=H^X`2nNzDRHlOY?1$8W+RZ%+2K0Rlq4< zf38=Xe(}70p?s>Wn3|Y!D9g2y_k_Ng`ccR}KQAM#yEd_Kd(Wfi zEwKG=CJiF#9Ci7@*}Dye$9AoIa2eR4IUq)e`JrPh9aE-F;(RLEfU1w-nvx@=#)Azp zGKi~ssjfP#s=Fpl|ka!XJcv$!|$(D1j` z6T;|J5?brku8MhC9H?Pb@{iSe9j)iU0t$vi0*W#+t=C}T>DUkPkLm>%7K7I|PpmdeGPI>$%^a1DQcKRGUiMB& ze2+Yyz_%TjUTl8esC_T8IAeZXeYpnDo6b6WEe|WF%nX5#yNvaXx%DciLp4etvc{a% z$`fX*^-o8m6B*5ekbV14vpu?;R2G`P--wr2Wm^eGCA-)=PXd6{v)8|CAYFXiY8i?N zGBGDL%$m$WsyEbfSKxe;et&|aDF>j@7p&KI9Q~Rq62{~izIs6g$M71K8~W?75n%C} zS^++mSGE@$nKMJ)K5grz^LfJ$^JOs&i&w5+u#B$TG@yay=bZ|>} zY&!;(XW*4Fw&qUQ98R!3M1z|$fL@-F)|8GD=sRb@M&4LSO!X*jISd*tRzi0^V^of<%* zKxZ^OZk`vaN48i0u)BwL)k3YmnaQ^Gt+_05zFUWXK=RYi2O;wBuS=|cOf$>vJzz+k z)D9MY35#^nPk^L7tVE5u0w#sDc8Neu2^D@b!LBZ%^YVHS5Kks{r`!NwSY|lYF_ebc z_MA*}J)J$InegowC^_}P zE8wUVb*xQV06HgchX?cSZDue{Si)3I&cg<$rTa`q>=O9*oi2a%O5XDwo+<_ZYQLYC zc)HAL+U;k8W8ELC8>fcv{SY5qY_}fhXV$pMhhXu`yvS^ZfypE#(anW5A)}yiv~;63*zn;%|64d|8ty7~+TKXyzqoC(bV;*{4Zq~wW$5B3<@h`R}FeOW8V`QId; zjvic%rNlc7TVm$X@+}qIUr&6^aW2?hX-SQ=W`3<#rfQo=#GvAn)=EKf)v?ed)4Jt1 zaDs>fju%H(*V(|!*K8qu@iwpkiWD#u^_q~|HU+ltK(n^$kD{A;)k`0nZo>F^TkpH( zaduMgPmTD)C`9p2P*Ir08reHD-zctrtKoMRSm`vgZcLm;{+aRA0H)UfC8{+MY(?@h zAe?*xY=F@<+GYIl(Q+;%V>aLCs|v4l6)7QZqHL&&J1)KTm!(!76l-uF8xI8*d%{&Y z&&)w+RdT;%Pifnl5DB)vIgUT=OWHVtXEHVp#7Tf*LH8M@D!2ne>on8jfj9(QTs~#^ z#Em&Op>Tr_MaLUPkH6m~-|_Vb_2X^t-s^_5{obfYLHSLtpw%Kt+VCT*Ysrb^<$%lt ztjF{@aI(>jBKDYUgh@betQ!%WSWme7V16q*9s>>PE*UcPI;t;YOVSh(gd3Ltoy@{o z773b~o*$O!5MZc)EDY-@Uix?DtpB995t?6d$`E2C&HpOaY{E-oqD-7A@VWGQCnS!J zZ*wQE3Fr~R6IG9=D?kqLM5s|WwY+H~CK&*VB~-kNfvlEqTou{CpMe0Uqs+JHhITwq z@bL^$6L-)MpqAGZGOneLc4g}>cC=v@OSws}tvD3Ud&U-n5Qsz4tV!7R^eHhDt9nM@ z>ArN>;+&%)#D>86dFztO6HBPUyN=yA%o;#0+Hu{@;xSvR4BxA}xiiikPmCHp$UWN@ zVVk(umP;JAeGFQf#9N)9v2m(_1{xprdC*DZ4M#Z{^gxscLJ*az{zGTmoxm|iNlG(1 zJ$6(;n+6QsuWyS(bp@6&oE#gf6Ou@*@g9OG6JrmhP06-CnpsXgxKJ~iQPuSyX>clN zF(d5PQ5AyK1B*LFgfeU1NqT@buhh8saHrb?ba^e3Wm1<-SwMOx=rfxdo-0}1N7Q=( zO5!4*?Dx6U%U_XSpymeYf8OXpGp^}|IZVd)17%%YcK1OD`yFWi|wi2hZY~B_<9#taG=_ugFh+<_JZf$wy53mL?H_!D=v}1flu5mdP~fC)j_yy*g>k#ym!H7 zZTFGU+JyU;m{*q~Mx92(2`J+`zuNpWZx_Q*`Nv|=8~Gt0p!x286M zR*udE1U_c)hCoggMQ@o}zQ0l5U@EHkF+SL=Hy$l zPm20u3wMcuuT^)faGFb$V(~jKMRmj0!_MAHS{(J8Kah|pF+{ga+fh#=4JGW=;)upG zf>UV9&l^K)BAEN4R%(c|6{tN2dUp9t^2{SyR#1mR(>3IBYsR$~hc^navI9?P{(pa? zd!q1hM7M&|Y&h}0SG2>hIU3WmRz|n+QhHTjgtf25_Us`u6bD+!si5_8eQ{x*^LEgP zdB^|@vink4#4^-Ev7hoFVdQawrf(`tNvkNDWzpH$r5NB5T}KG;G5r3&P}=)Zhwr5e zW{*ddK?^7~jypP0JBxXb1(LBTAf3?USNelGdOgQn2gSS{zn`3B@7P_0HhhY$ungk& z00N@F3glU!Q4o~CfU|kZEj(uE_$7uW&v%-QDot-uHo~U05;A`?s8or(>T1MOuM#}* z7h^7<{`mXbJ;mXEb7t90mltcPRmmFWj+k)|Z_oVhsN?t7@<)39E2FYzE-ZqfYaj!s^DmbcbjVuiK2;85nqrZkxUtNO?CkFnDq!ZE%5 z8Bs5%7WO-^asw`RomDQvR_M%8S!N96kgYE>mT7q4ztSFXW{1&MW&>{uzEVTjLUIzg zA-krl3IBzeYMyD`#RU9PG0@I~GOWGG&Each0Uj9vk-ys|x`6(ts%fiR>eS*ta5B&4 zAAcga6;9OCBHB_u!n9!HdU{HIOY*g#7}`_KD4P$Ve)A{LeJ=l)>R2Ko@@Dn78u)+9 z=LWfo}&~T<>$JP$1v zLG^IrdS7(^8CE~XQFU#Jf)%w*?XyuLb-$cV;MbLI`7X!<#O$I!;P0m++|*3C@MEe? zl7-2eJ}DN>tB`6IU(f01^k2F2qH>|h6z=~!=w9W(;8JcFp+}=gy_$d;xbA>s|HSx2 z@!hO$e|ymMd(JLWu4h3@q~;+EQxPUNw9rqW8ft;FI>CaagY}myZz*sjjIzE%0joLsYQf5HM@&CmLX~wN2h4VN5cX#f(S@PZendAQ>^8fpB%_RQ&whzXY>K6w z-xw8cEcA!Zj?G1aPW1yIsc`MTsgf+mnCm^OxW@;w6>gq+pcLFRI0S$%jf%GttHe%z z<~h%sipgz4`rIS%P!FIhT>HNSr##QYWK6AS`Ak1xlj&ZG*aqKdz~8Nti9yF@TXm3o z-#{%%Un(WHbxZOLNg9Q0^0xQUtHvkg+ZjU??@Q$mb#GqDvB1!7{cIo zBeJ3*Oj$DnI7!E>Gsx#=VXnhMGR`P=Wvlsp&pg=DE7g(v2XKexS7F1Vv2!x5Cb|&G zeT4!?c2dIpNo818MDOSzzsr6v2K(iK;w%+BZuu0l3()w5o>`%eq8Nr&-D5j!Id6c% ztYcH+8u1)WsMPpvKwIzZ#aOuy3o0mcJGvX7s;aDBoS=?=Fjc45&2O&AF5cf?RdcD;dcy1F${AsgHEBH#R z-~ZXb$FC(;+aFRcmm&~J&qkPDIghW*-?v(@NQ5OkJoH98=bawv9lwiFDz9B#*wVmx zt5yvDmBiD+so?WG@0}(3uJF>qhFWku#nldhSSCK=&?e^+?D`-1>1%(7aX%t8zWop+ z=O?ygcr{Lr?VvL6=xCxH1hK5b*3FaWHre%W^8eew(~{vUzttsNi(v&kS@>EH(z%jX zj#1wqVqLVos$0Vsa^O3##w@_}{DZCDfA;SqOdyPA1Phm0b+!Ytz)8~B5B=x&|0J)2 zUh3`2y5qDyfnu`VlOYM??!}zTTNIKxaPf7foy`iVNf-6IEx@X*xWSAd$?!#beFJ9) z_$^pTN0=pP;hxkuKF+?7l?k8u zE!%wIcrYtoh%J#n;M`>tbRF@z-KOIo6mePUy2$bFCpfx+;UnL zB{IdcE zHt9t8W(8z|l{U?99~pP*0c|niRY@5)L#B)Io;N1vKLjKo!Wb4D6sJ~%#gz)}8Bb9d zTQ|XCg*j6#K}SgGeyIgbqpy?U77R;_`M}PI8@s`;^2p8mC$!xrT7HDY_@V0056Nsi zWKy?Gi%sA!o{-x-QeVimf`UGqrM&X<(++u^;^E>2isvYIUY~)zGFB>-I2w%j0e06ER;Q`EOqKeq;O}*MVcgCVO6MQ$63)i6z zllh%a470zZ!-Y3OG<^?++vXIfr4pcv@KHk(2(d7w`N z4bI1jSClFx6|UX_Aj~ru|8$kY=&Oh z!v9C7*R&k89mAM%VAU#O}% zBQzOIJ8Pk$n5+eR2{jqyeB6S=n_xv~EHadZp;WUNhW8!-E9S;!8AQCP>XsT00;bXu z9*+5`uw=v9CY-%_sGtGzMFoe-tmWS4F@&;+A6o3B)eInxLy-kW(DYi9?T$bFDVU$+ zC;jN=?j53U&3C4T$~&3s7*y!lK!5XTzK^wR9AnJOaVd96zy_vA$r@0@V z*Pc-!j@XnK9~cyI+ioSmLm5^$XO*`ZsF1YOdAl;%+}N6#*|M-RRw?qLcqCjD=ph*V zDRGwQk!m^Wynp<8%vyP*LO>QK15sH17xi6P`xeV>(8T)_9 z&#OF{67KZyz=ZadtQ~_GJ{caa5;g=#3xqx>SUn|^YlrFxh6>?Cx z)-0gxf1a4g{AhH5PrMhI$59IvbeH;6gBxR_uZ%)(ree0tPu$gqJISZdiZ8dJZhjOu z+Sj50=#;rX({RGB@+M*kP^_=oo~WG}{Q$^f!{Lw%fU04FrsOU_vp#4E{W_<8x+}YF z7nAJiuA&CRcm}PR{=|QGUWJ$|()38Ij8=hlqH2iu01Va)ZQ$L`6i|yi{@gm#N0RQ4 zSkb9o8%90dG#@A-Cl1N8?xzk5x*}gglqWBWXmv(IVpjbsODCbUehu8B$TM=h3G?|O z5nSn@QO66C#Fr%F%PGdfYSgO}OSbcQ$B3m0iR5&z<-4oexpl#YWSs`>+8aUDsJv`1 z^VwN389t>+xcD^#ZhGQ&O&8mCo>wbRa!;#yzI5Ful%E0QE}D7XJgndO<_q(|G_;dv zHLY6C{CM6!y?=gw7k+3{k5o z0{YeKr=L-hK&{h%%aCLgzp$v29*-Fc8Imz?19C2Y0Mu*~o)zw9DSA3}^|blxnO(9w zs6*afJqdXFDJnGl7t;MXzeU?`>a?Wvf0DpHD-ACRG=1-OVuviiAt~R(y z71r6So$fi!W)G159ytuGNWjzn8M8=`QRGft%X&c3R9Z24Q}-a4EqleRdRM`@R~>@= zJ*V8oySkoPCU!mdGajCBFZ{Xsc+vC2(cT`+;WzcRs3yJLi{R3RzGcu`^l9WB^IJ-! zL8B1EHXV~-OSJNY!-Q_@#(jiPqFEomsw#PIq4siRaHVPsoH6yG5T!T6h=2=65 zBO=7x&ghB0T=ePam5Otmcg^Zs;c{N}JoKa*u#Uwq+n}kI?=+Q29VX`KKfKW^OGxS7 zUa`D4fU;Uv5r1vI0-VO0o__YDRppW2S-qY_R&M@1);rX?L-kzNqsM`(&x7^9)ZL~* zvX1Z;(XLKuw1W#_+j3U0W_pvdNW-c^b)SBv)v6Eo2p500KX7|unRfNNjt0O+5P@@! z4q}lBUm6%Yqp-Yy*tnilLo_FVvh1F=mFVz zJbEkJ3dd5d;f91d+^_$~Y-DycL3-R(6}cr%p`BjoJR+4?_~98MLS^v&CR+e($O5WrCBOba{w@VV|v1e#r#Cm-ByHdvUWdZ#j1;6!ns%E%%0 zvy1vbX_5b@Tq{3nepj+|+O25aAJ}zDEA!>2E&8t1W45LE^@}HQZ=8Xc6LLn8L-Keg z+@N9@aLe=5$f>jMoCtgB1mxx&D6(DG|0&rE?*UzQX*B5CRwVSp%$BQ1LzD*=S~+Pa zbGsnQDPG9^r9k$F&qZ>OQ*m^@e|0)1h01cQ&y1l)r`qnqLVi+3UR0=4J$ag z5Lg9A>@gYa(FiyZu zUU0&CJ_JGmQD1|UR>^H~p6^fEe2Mt4|KEBalWXk*6kF%*k)(_3_khOLbX`F}Q8wGV zbYbF6IeG1a92@&F93m87z9*$Eg41dQKV4!kzKh*Li@2$aXm<;(;E+8}4yeImDfPU7 zB@r84oD(p-*K1xS@gc`(wm1h{S8P^P)xEyAOT7Fl>Wba`tX}T`o5oR;5|X7AxSo*% zBl$rp#jRhta^JQeWUnO}Y2|@?`9`xH;uffj3n6m9=plEM2e#)Z%hfb$%OEbsoF|ae zgc(|aZ-MIU7Bow)v82S0GX*5s1VUiv;^!q*zGhO~*cDbQr(-Ht5bNCsRMZT`^mb zcQ*s~spSykUYk`rUNSVx<8L&-D%m+0)3)MW|n_hAK&7tL|BkAS41fe>`d(vwn zYDdh!osKB4C@s2-{obx+s>`)fuk5X}$zUBfqak?WWJ~@Wv|(K%e)jJoD{ z%zj88{yhKyB&-asl!3`b?FbZ4X_R*JzDYxEWi_C4>yT}&aK^K=#rh&H2Vt~G&$U64 z?SgW_Ib7_ls0^F@Y1afI@>Ac-&i7MPk{0X+sI=Ut{fYAT)^-lBU`Bz150qDF!frU5 z@YSX+|9#?oiTWgyFn%p)4LsxVjrjQOs)-gi2MVo0f-Jt#RapTOCeW{fEqkp;^kZ~r zJ!qfksv%r_o!&`pR<{-i_mdjw`l+k6IsN*bBka4;1fS4ob$@CgM6ZH8Ze zG5Lqn_Cp=mg=;K4BA+g8B9My|yD~E+pQ^1xI=o&4oLn?fym`=6;bmyem9d~1K z>@P7Hbe6`?oDOY|cw%~+*x5k4;9SshPU#%Sd8~G%KK_YPVkj$q9>q>efjv<>A6@HbnEonn#-QRsSxQn0t;P#5hB&*j%*`qbntp)5AtJkMNHb;<) zOadwT*BdFi*v^eKkk*)4SIzuOsd{)nV{J!`q)6S7`Sd;L$Rmm)hoJou)M}WF42@a^ zmvMtyzE}Y_?S|GQPh_R4CQo8umdoKwSLEBzDa-kbD=V#=on9i3M{JAs0BmJh`(%2a zIurF4C%z^qzTj>cC@NX{mCF7;cjmmCN*gW3Pk-V(znj#bib-*fPA*RiB}krU_Wa47 zZN;(f#^qZp!^m>}KWBfutv%^f8C=*wwEw5cDr-Y5h3r+MzZ#3=#CbOU$6Af=OCCK1 zqvLH6#BbE>!_E_tm4~)@9l^OUrwLj0h9_rcc^C@lJ{|$JVM1Qt2sSEBx5nZHG=`2*_6C&nYOcPWCOe}OP)KDf0xcx^+_w5t^} zf8PmswW;9iWs9|Cf^Y0aDas)B53GGVTh|@EJ3BIBqeO-EoK`Z07*V_8?}-y-Z=JzZ zsWjvV(7+cMHn>I{t`&D~u71tZ!>Gt?#P)EvP{UKmM{M-Iw23}LQTS4Q8I9ndm%e_gz#?+)UWp!7+&W;J#br&=0+rZvJ2Z;9r*TBS2b4L zi_reDX@(>Or$OgORU>#yV8iy<^SE+T)tE?3*&LpJzwX=4k=1$uqmz&_e;tN#9BE-T zx~JDn24qR^G}=tpQO`}vGV`1W%yk;{-QTA@%Smw;tU05Lfs%(k0;-!lZ>EUxqTC1~ z7Vj*J9El=a{HQWptIKynYjAPH+?{ew3Fqj2iTSiVn=YrKJfOLN0cBXHiRekNni*yS zIO>Zs$)P12#t5sQUQmQpiTM^w78fKi7pQpe&1-`m>}O{A{}jW#2de*fcSJ|LTrZ^T zy6b|{Y6vJu<*u0W)Lc=eJJH z4~-5$lvh|n6K8?h1I}?PBKb)pPUKV7e~p7=t6I7pEdzb`)fxzhZ6Cd{jqQsG@z90? z@o|Km0iR(%peeFw!Byqjc9!=Z6JO117~}6j-rd0LBMs)f&yBSdJ~wR0bY)UTMD3P` z#pSl$fIO>K<%I;t_P4f{D8mL>SsQH*Fz%T+lQ zmj5A0w-3EM&4T#mMJ~3mENy~lk=|1K#y>jjxqHC5|DhOxv#HhbFP)-?!h%)%0g>#2 zMbcHSt=(_~NB3)OY@Ik)NXn|AWQU1m0QtJPKrF+9KV;zfRA>gy^*nh&`2(~4UsUw} zMoieV1yjdK-@v>X3+&t7+OeL0RDr&+9!LIgD!W*=C%1a##DmnuVynd!@kp=#dgL3b z=P(MDp#Dy(u8KpUyp->+8LBUjoQ=kb(Q)(7=y={|bR4%LpnNC=9LLM43lCE1@0d6N zd0_p4{{~K{uURuH=r|RTTf)J29jtd(aGBQ-I*B?Sw^H9|$GUUGHEbHFVa3u{yNb(e z4ZpD#K9n-j#}``J0*2xNTl5UvwQl@UslaKms$riP-hcZU-Vff`=h0>iPm`5_teh}A z@IbP2q2MCp{SxICs-W^W>wv7L;4twtm11U1JdOKw^B>&&&0r30Z>OhAJXbD(BQWZE zez0nm*x9Tule*QdIMv^ZbV^7HaL@L(PuJc}UA%tHo}Z8J9hCpdckF-Gqj|4nYc`b^RKI_* z@7_mqU^ip4g8uf6r#5Fhf@T;t11B*-Gak85wBG-8gif|ktOd@^Y3+p1>}RpPt={qd zQ!V%;!jr}4wa%|<1^Ml~79^UtIIgaF@DjRtd+OylR$b6hh_AD+ZwH^d2;L3e4m_K& z?A~0^1Uzt_Jfi*km**>crWi7`o6qx{9t>1I$z(&g{CoePO3gF|KHw^jZY_`@UR^KC zpO)13obvW$F0j~E|26#WEkYb~R(3u%Qniz_7As{uNU_*gGKthiS2#oX& zhD3TNf|x);ZvjF;NRtG}OPHB=@4N5!-g^JMd)Hg*tp&;Vd%owp&))m&c0Sp0mgYu+ zKcD`2-@bi!R6Dsp@P$b9?UEYxGa$ScNpAyYwt=jkza^6 z01vtGTYKaDfQFMfKz>+f&h1*4C3ngyRt>S}n}8@T6*S?}3G7u%UOc5r z1U%PGLj(3&QN@+`PPFt+{lv15NX}>pn_+ z$D4u#s|*lNtJ>I$r*i2mq=L_J*R6vb<*~La0r8x~g=P>4Z+UbIC4_nW=YSQtQy8Rl z5VfweZv=O!fJ^5;S463{7edl{9Hr>31q9;6L<~uqv9p+T@+Tp&CDJXNf5E;eV-d7$53_WfP zo9&X0-X1TM`H9{r9&H1+7a_}-~q6;SN?4^_Du*AroGFH-=4PzJWG}YFJ z_Tw2!;2-RDOf+kYgw&tHfAJ7myx1!hoJg?me6D8Ks=aavqdH+xkq9WbAK!>{b(G|6 z-&drlOlVaqlQo=?-s@IF78LTS&TJ}xv{8=PWm$(`VA`V83xnHb$q^DcuMst$g7i}x z&lVv_uaw%i8`kk{URS{LYRF+>b62&z*Q@=&GglUzE0*c z!N$>k%~Q4Pa--^6q>cTQ_ob^Fqao&ti7Ad$L*c}-bvt>O?u55#gJorW)SRWRwa;qs zyN~Ml$u8PFHG+_-90);wT)FfRphr}$~-X68N!dJt4=W8%DTx9cKcb= z_1&pC;xB0L);}SzbrdT{**QCN&a)P{s<5ab&R76u(`h0yl{ZL4cOh>1a zXwyorXmc%8|7*vO%*k0bXJQ`AWj1nIRJ)$Wf*8l*rVjE!GZYYly3kh4?~#OXJ)0&Y#Kys8Mp+ z9b;Dt^y_m$xyw@C-ui`g=&pOT{;i|t@qEVx6wXs38HNptb zxHfE_fWgzwLWGF7Vdq!A7_Q{rlI(ou>vauP=v(36yAt(r^`e#%w3uqV;;skG8LE_- z`;8>IlDMdjQ*$cm+irQ}gWX+=LqEtel5T4coOKr@X$J!kpN6i~bSbcVBmW>JHM#1( zjBA3%9rjxs)m~qi`r+c&7vA~m z(Xf$7XDQa`?b<5z-r~GA{_p}+!ZQo$LKxIB2SDGIsE&mFM)QANTw1?2e?7>qkLUX5VKsUe&`{@-#Cm=o4ttUGfg6=dTlu#-YH}w=OJBOu3P6&^A=yrvb zs%>QIjf~!yMbJGJpk`YD&n!IcT35^NFhT-^#GE?VYln@f!)|Qmx?P5S!G!CKIS2PR z9O`9N&T9}SgrQ$FVLH#-`FG~Vvy;gZB+ss}zNUWB_FpMgmpHQSRg}gpzx9aXE31gQ zb5L>JFBL+#vsBZZlqB@+&I=2{gOu>BFlWc%$-H-1p2Y)5>S|&cTC8A604e!uDbc;G zs4^Oe$~R+uhC(OroX2VtkOr7hzsp($FttqPsPtUx_?PvIw#deEW`=gH-y&h891L&D z3osGCp+D@;01{D~8O(UW@-d-xJEs&Sg22xBeJ^t9wJ<>j;M3IhJ-hHTn{9bHR?~DA zzoN*bkxEc@d%%FE;zv=p`Fv-SbH5cwtj*}L=kgP4?ASji#swy-s;AZZb)pa5t#kZ& z^%dIkkApwpbEbAVqlR6L3XIZnZng;gI?&qK8#XlOOUXXvxNq29m+KhJQ1kH$GuP-V ze%B*t$-!6TRVcUodOwzsyV3*GAlBtrQ7F1s#};;K><)$c!09DuF=iwuqwl(1IW~Ra z&|BD4?kJ`%Y+lXb6-UmSE^L#bdLrVAW>XEGbYL)S_G!T+{tQ*DQq;2$Zy{~#6`8OW zoAyO?1vhFj_JFMibB!wL>L%$ur(IJ+(ogjzqAeAu*rG`}itY*4pHSV+bdBgQnA4c^ zWPI?vgV6ods%&<~MOHQhh_Zw=KG#MimJQJm!Je@6QtV-nI*hUFHu;finfR;#o8nnG zRs97o;ZX;?7Cip>DoJd`*0LO7;IBY+#)P;k8T#tQmhNN|SW>Yqdfa07HCgdUvamuR zoGyG#bS`T1Y>PPlfs812D1m?f5YbLsO@p6~4r<1d{Pb7`x=+RxPO&<~j2K^G7m0ZO zb8cgYRRHEIi7boeaBy9r)znl|1dN{)KAZ4NK*+7~Bo<^yHzKJArZ|BCqIp)U9*+Kn z_T;vriGKaOtfzWf%J0l|6OEm{`qdOh!uB`16sOHfRzq3}Ag6xlXs3;BVB{F(zEWXF zg;n#Y{;s7_$dHxhHbq=AV`cnF#5hQ~C)P0WqB;z}CBFKxuCLPLSnpLOI=@5&^!rNJ zQwjXtYieI7W!}LEco`S~-+%gA3t(WFp1uUZ4Lkgzu>s%mES3D?f%8Dm1_xREW`%5` zA(%=iEF)MVP$@4=z3WStV8+*BCTRWOSEQao%%=A4au>jpddlbTl(SvIGVqmESQ+Ne zk$yn9E1=U$1`6-wHyDa<>HK`OqRd}t~r6Vi_`5*zjSzBr-ZcHD0c49Rpi)nIo}ah9t6PJ4NQcs zsMJCd^eW=>^+}e6QbzHMPjbHupuR*?E9=$^f_F%7u+@JluU2hbfQ{#cgRqBXc;*!V z#shn2_OZIiY$>wOQ*}ew(;FL6EEgR05mdINi!-8FBdhuWC~10eFZ38lT8kD&$%asL ze`Xazbyw0!;e*>FbE*Mj*y!GHA36EPwX%Ujj4D zAtlQ6$s-`jqSP}zs`iQvqR}hJ0W{a_UfOJQgHuy(BuUK-vc0H zkC8Yuw1i%;La%}qOq+5xAGbb=mubn&QmmuwV55ock}%`M3vo0x**})R2~%LB||`kmk)L_+6iK7&vv|d z714OTQjT}O{guUH{Ca>bQk+;zy`qJUt1Q@BlVV!Sv|oVQwgxcKCAC?>HTLXwJ;S*) zR$j0-wauHWGZ46t7}l{w4e1tkY5~`Vma6 zr*UBG;oA5iw~{#tHy3s`q@)kk8kXHecaX^Gy8$41y%{ac_Jybv>hSE*VX4ZjGq&-0 zl^#+Bs2?gx|0IL6LljKKpAoY5v52544?jYv6?aAPDjiP@_!=(ZZ3hxBWfZ)zkxw>Hw-3dxp7wo=UdVTo z6zID2vw0oKm4ahgD}T$&-gW1!#%@rA6lih58K$-?*yqf9>iJ6hQ|0u6;QH;KqSqpcLG&xV7@j^q+bbr&kA#2b5lx{qrvJ|L`|RG zsDdvyU=AI&%nd85OHtE);X8_k{knlCjdiOr=3siqY9|XIPEnJd-W1XV^H);mLm^(s z>=O&j4M+?u8}<;npOgM4Njc!A-HO$VtE*H44`S_Z08#2cxS{Ex%{@8a=qEW<`31Z|@^)w?qQ7J)FR2 zC$R%^-8$NteNDAtLd!;-4@#xuZ!F4-BqGC?EighZj-9U=8ue84pzl3m|7)dq0XjRl zU=35hVZ1c8cMgwBbn(0?bMLBARpR4!DhrMW=%Sr zO&)vo|10#TJ62$7_)PNWn+If_iM{yRO}gR=$-*pjfPpu;`Mp+&0G_dzTed;B*3y(y zxCe-d=v^w(BAUaV(%+%@M@O6wvo{34C$(WW;7_~mzW=Q+`_|$m`DhlmM%kevIg#hL zS^-9xq-+Q@e&T`O&m_fv;G>g<(b5Qv_{V4uL->>STf32R0zxL+#XVOxuD{WL*AulQ zy@SI0r!L=`e?9|?-ku|EVzw7lRlO(EwB^H{wk6`~?PRsbYFP6hn9B%76MwG61x<72B-9J#b*f}HS|Wgum!jMkOj>0? z7Lk_no`e5jTMMn7ANvpn!xFO{09SVF!R&nMOGGVzBozIl%|#>;0(%Bc=!EC5Vs`1} z*mb#qvD2#ZI{6ntsO>pv&W~-hN>tmeVL+THDJeM5KXiKw8@ZGiR42NJ#)4l&b@L>FrVIQ8{Ju#%y-A z<8FR|f_5vwb*>MUjF|K_Mvi@ zj_iSm4pYP9C;!i|!cE(#A2Tm3rlQ!CJ_W}t9>!+Jm}z@~oul@tVo<3|rT+>j)Dbx$ zwQK6%RjIp>t^{hGoA?x>D#v8b!StinzWQT!7}oD~xu_-O%5yHRMJ7Qjk86=eSo%A( zub^e(CKapCi9YInqPcdL&0Kdj2{wx_Wh|s~gqWw|JNmba>%ITNI42EbBG(T~3QN^5 zjOGTKre%D$WpnjYFH@0(xbAwLg@5b&m*4Y3^`gK4)C^eM!2y zJM6NB6hHVQ6tKvtHfoJ+zren#rB4p{3tsIdHsE6u`C-IB9}9qdpOo6nJGQ!09nt$} z%6DV=84%lkN3S|}>Sg?aG;yT^O?7={?Mqq;@;ZKfds??|O2){3*cu3k zvL_&Rg-$Xx$?z=&t;2MSyVi!7A6cw^5H0R7Vhygh>>Zz6H|Bj*SQNELJVMu&+WLC* z{sGiFKg_D%2Ge=A=+3%eC}m3NtbEF2mlZbxFs6* z!Wcz0*;epNo6Wk~>i`e;LCkMH0Im*nMkEYmM4ahw*MZm~KOzVbePUV4AL=HXgmY&3 z6h4dDt9bGK1Um2p&nHZ`?egwjxh2$sxCgZ71INTu{&ctH&e^ zqO5QS2l7FFvR76{>}VP`Bib5#H-JIE&JA)CEkg||$ zY6-}6Bll|jY3fcU)_Tsn*h$vvy1p&8RTE`MDD|xnbfd}HBG8QR$Tg;tQrwo##;X}#8FHRRqsu30zni#5R>~KI<*1}#_q=4yX?{7gDOnm+C^PniNeJ(72icM?`;{X zH8NyMyVGm0!nqC36Oajfw=TNcZJ_z&TGKtF1C?tsTH;NDU5r$4mCt1%hZA2Lk1(6U z_7xZ5rBkao;X@W#8Q@D74zdeSlO+zr3OV;zYX+Ra>5!Z|vbif}K7LW&@f(euNAVH8 zqiEGsJbVBBmw@;Bt6fWnA`KaDz%4~NBYhjL$KJwfr`{?KU)T|NM zT7S@RR0m0WGI=Lo6=cXRY#xH1rmw^f%IOp1I|yw=^i#9Sw#X2Hp&%^m# z*h9L)C+^f-1J`g0G!2>8f!gI1RfrHomc9Z9J80+DzuXwzgRX|e6s~}Y2elV=O(j?hKXYJjnF4%i{>Lh$d#feYCNX}3bzsS0 zyTBAjn49}xcXmzP2;X&98ifjy>djl%Vi!RV60Hr{fW&ySZ$#MkHVdVxp_zyDhTGRl zA@L$CanMfV6-F^P?mAx*xuS=dl^h|15rDX75qVPjq**WdNj%{#iV;21r4o!wE68Wq zw}<{?(&yjWn14@@T?$NTV@KUO;o0}JD@uZBlh{)=r1P0HmTrFTtdM3+ivLU6)QQCp zj{%RvYt#sL7R4C9=2aPI{SQu#H$H`5(&@*0UsC+2*5)BH>s><=cP7YHrZ=2>PL9=j zS=)XUw05cpANQ7xdx{5-bLzd%aKwi?0%|njEb{4>p1V zf`O>=4*7*z4hULe?LiZSDY0h9LvuCu963HEqmr^er`+S4_^S(;-@6UeTPFM;B~8_A zp4NC$goo^9C`0OJIkkaDBc$0}O8T|%`%mg@vD?W4k#%JDDaTRO#!)N-2#!bm9qJ9? z64_Ls7_H%5NvG)v(eh;3+s%V_#xEX!q0}wWx=U_*gnaEzikI-$&ArlixEKtd|7orR z&HWL;XkEPM)+r*c$rCe6EsEaSDp0KkA6)r@A5W?Lj`n*2imGeYkI0}P%ewwDYQU>{ zv5t@qhor3wl?^BFAllh?eLf^~XIF@HWz^$WmIVI0>+*?$LHB|huQRT9YnH(H%#dP7 zST!0f$}7LNy__cQXsa6MIh#=x-lh-VQPMs6d!7JWPLw^S=NZ2-vKP7EcX|1RhAEcg z$xCY0C#lm-`ma89YYF1+Ujy>xV|DkB!4}{E8R~Ema@8Y9XqY^VkyR6eU4RzPwRt3d zysy6u+H+N~xwCuMp-8C-OfX{GCnB2B`cGu;Bq>hHP`ff^GgI1)@8@CN;_5R(onM>v zdUH}fQ>X`-x5w_R#4pONR603d`orGTw>enhZrbZ>Ej{q-({BPrRMPnFs$}}4oH-== zqvqDMC8Ew67whg`^@R-hDuqOrbH@wABg@P;RAjgQ7hB>mg z6-gQ@85+a#AXeI=^B;q`^*Ul+st|8Pu$W4E6jbh)vtIT#yirxn zg594}>y-E`aHYakDhv1lEf@dwuTxN3fYtnMk9a_f_n8en(e@IHq(`Z8#@0w*qbI<} zRu=UwqhxQcJ3BmN-x~5UrQS1;#4W^R2wWt7ml_XJVA#OqAV(K6>=g@rQu&sJ&KXbL zud_G}j_1W}4lNxdD`La-n1G8!X5&+AYBkpHvV}XUNw1=j-MRmIya9%5?({)k z-uuPcubz~>;RDZ}Y0014@8vm>r~WT~c^^L{Bp$DfF<4^xgTfXdI|)AD?GNJ|=Vlh* zT=!5L)=Zpv6ftlLtB9dcroLQ1VIZ$l)A=8p2^w=E4YZwOU5lOb`VaDJEk=IGrWa?G zTRUm+s}U6v)C>uoYU6%bz}(pU#dRgwXmw2>pPD3hQEkR32hVI|i zIJVgNiBo!B=+aiHZ9`_hy?@o>7Apz5rlH~%zsW-(DGX}d+ie< z1h$^Ry1?y7S_W|2ko=bBx?o83)?yY%b$tpGZC5Qw+5O%oq>41J+~u}Wgm2MmA;do- zBxuzp1g>D9 zM$0Q?M8CopF0p6V zGd*!5HzrhoF`1G37K)|M&s`xF`07;!Vc45UtCU{>iAxk~1o+6L2Xw-m z$5z<_SwSB7$wdA79+Qq-Ca=wkALHHRlQm|}^45bN>w5ZP&y=(!gr$aKi#dHgQY`(K z;rzQTR;*95oG-<^r<4JOUzVRcrJ$BLdHh@iyz*f&VX~HZZ1=_=B0Mrphq*5cE{Gr7 zD!;?_psB<8w#uE8IjwKxdXiX;e{v?gPbj0fkLYb`XOX*|j7)n?9492a8B@+I-@QzXqq|MmxFXMeBPfpZl4{EfQ9 z^siAX9z=o@Tx9wU)+fauoWC=vkRL@c@>M0eVwV0L5lbmn*z`J=gCgK@*~)!5xnip+ znI6HkNN*WmYnb{7BNV9#Pw&?JeO=fZSr;Cm1oSDP+wqj@*YWg4&@G*5g`p%S&@jH& z>-`yH`3k+bXCqjx^D)@el!~{GUqsv@&Fy?)RW!M{#F`ZCJiW7YyF0?Cfid~f-v7ph zijIy5>jiq#bgrLDqde*1 ziVJ@%KQ}%t8;~S>YqC(M=O?F>--?1G-ao+t{gPgu@l19Fngxzj)pTg<$xw#>sC(x$ zrLt2(G~up4oX0iN3Cj~lisD|&iX}ZYD!pc6dRwnrk0R>=zcUX}{>V1F7VI2`3`Q+x zNqg?Cm-~dbo>$d-dGXCcnQpd~dd&R6xTGRvL}08d5H2y&SFc}{Shbp0XVd3(g0Ggt zlapr3i%+Rf_w&EVY?y|k0m=Ts@oz`b#oI_$=eh-nu`oh9Oo!1^7ErBlR1y-(&!u!5qfk8u$hae=#iui z-QEe-^?7j4TJzrb&)L(n!^r$k{IBnJlDQz7K1Sh53!Bw@U?sWzOr;fvgcvbc*`v*q z_Kkn=nRRcw9Z0IID!id+YVEBLjdbz65qjrF29KlWV4hW&#GK=PG!$2T4EXYU0<5GDS(@K0d!;DL*QP$o<~DEo z>M`4o&~gOT~--0~ Date: Thu, 30 Dec 2021 20:36:46 +0100 Subject: [PATCH 05/32] Add list of guidelines. --- doc/source/guidelines/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/source/guidelines/index.rst b/doc/source/guidelines/index.rst index 291ad183f..da17b60c4 100644 --- a/doc/source/guidelines/index.rst +++ b/doc/source/guidelines/index.rst @@ -19,3 +19,10 @@ functionalities such as logging, data transfer... data_transfer_and_representation logging service_abstraction + + +List of guidelines +------------------- + +* :ref:`ref_guide_logging`_ . Guidelines for logging in PyAnsys. + \ No newline at end of file From 51317561f18327c058694ae0bc5ca0497348ed08 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Thu, 30 Dec 2021 21:56:29 +0100 Subject: [PATCH 06/32] Add the intersphinx extension. --- doc/source/conf.py | 1 + doc/source/guidelines/index.rst | 3 +-- doc/source/guidelines/logging.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 60dc75351..092929a32 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -20,6 +20,7 @@ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', ] # The suffix(es) of source filenames. diff --git a/doc/source/guidelines/index.rst b/doc/source/guidelines/index.rst index da17b60c4..da05c335c 100644 --- a/doc/source/guidelines/index.rst +++ b/doc/source/guidelines/index.rst @@ -24,5 +24,4 @@ functionalities such as logging, data transfer... List of guidelines ------------------- -* :ref:`ref_guide_logging`_ . Guidelines for logging in PyAnsys. - \ No newline at end of file +* :ref:`ref_guide_logging`. Guidelines for logging in PyAnsys. diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 0cf522fa8..4f8aceab3 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -1,4 +1,4 @@ - .. _ref_guide_logging: +.. _ref_guide_logging: Logging Guideline ################### From 8058be49ae50e8d240a5abb06864a70eeceba631 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Thu, 30 Dec 2021 22:09:51 +0100 Subject: [PATCH 07/32] Fix Duplicate explicit target name. --- doc/source/guidelines/logging.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 4f8aceab3..e61bfb165 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -9,7 +9,7 @@ This page describes the general framework for logging in PyAnsys package and its Logging in PyAnsys =================== -The logging capabilities in PyAnsys are built upon the `logging `_ library. +The logging capabilities in PyAnsys are built upon the `logging `__ library. It does *NOT* intend to replace this library, rather provide a standardized way to interact between the built-in ``logging`` library and ``PyAnsys`` module. There are two main loggers in PyAnsys, the *Global logger* and *Instance logger*. These loggers are customized classes that wraps the ``Logger`` class from ``logging`` module and add specific features to it. @@ -144,7 +144,7 @@ AEDT product has its own internal logger called the message manager made of 3 ma * *Design*: related to the design (most specific destination of each 3 loggers.) The message manager is not using the standard python logging module and this might be a problem later when exporting messages and data from each ANSYS product to a common tool. In most of the cases, it is easier to work with the standard python module to extract data. -In order to overcome this limitation, the existing message manager is wrapped into a logger based on the standard python `logging `_ module. +In order to overcome this limitation, the existing message manager is wrapped into a logger based on the standard python `logging `__ module. .. figure:: images/log_flow.png From 4abfee26eb2a5a5fd52d0ffc95e1219ca256a314 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 30 Dec 2021 21:03:31 -0700 Subject: [PATCH 08/32] add logging example module --- doc/source/conf.py | 26 +- ...ansys_sphinx_theme.samples.Complex.abs.rst | 6 - ...nsys_sphinx_theme.samples.Complex.imag.rst | 6 - ...nsys_sphinx_theme.samples.Complex.real.rst | 6 - doc/source/guidelines/index.rst | 26 +- doc/source/guidelines/logging.rst | 135 +++-- logging/README.md | 5 + logging/pyansys_logging.py | 479 ++++++++++++++++++ requirements_docs.txt | 5 +- requirements_style.txt | 2 +- 10 files changed, 603 insertions(+), 93 deletions(-) delete mode 100644 doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.abs.rst delete mode 100644 doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.imag.rst delete mode 100644 doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.real.rst create mode 100644 logging/README.md create mode 100644 logging/pyansys_logging.py diff --git a/doc/source/conf.py b/doc/source/conf.py index 092929a32..9cffd61f8 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,12 +1,14 @@ from datetime import datetime +from pyansys_sphinx_theme import __version__, pyansys_logo_black + # Project information project = 'PyAnsys Developers Guide' -copyright = f'{datetime.now().year}, ANSYS' -author = 'ANSYS, Inc.' +copyright = f"(c) {datetime.now().year} ANSYS, Inc. All rights reserved" +author = "Ansys Inc." release = version = '0.1.dev0' -html_logo = 'https://docs.pyansys.com/_static/pyansys-logo-black-cropped.png' +html_logo = pyansys_logo_black html_theme = 'pyansys_sphinx_theme' html_theme_options = { @@ -16,22 +18,30 @@ # Sphinx extensions extensions = [ - 'sphinx.ext.todo', + "sphinx_copybutton", 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', + 'sphinx.ext.todo', ] +# Intersphinx mapping +intersphinx_mapping = { + "python": ("https://docs.python.org/dev", None), + # "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + # "numpy": ("https://numpy.org/devdocs", None), + # "matplotlib": ("https://matplotlib.org/stable", None), + # "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + # "pyvista": ("https://docs.pyvista.org/", None), +} + # The suffix(es) of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' - -master_doc = 'index' - latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples diff --git a/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.abs.rst b/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.abs.rst deleted file mode 100644 index aafd4ad8f..000000000 --- a/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.abs.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyansys\_sphinx\_theme.samples.Complex.abs -========================================== - -.. currentmodule:: pyansys_sphinx_theme.samples - -.. autoproperty:: Complex.abs \ No newline at end of file diff --git a/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.imag.rst b/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.imag.rst deleted file mode 100644 index bb88a8ac3..000000000 --- a/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.imag.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyansys\_sphinx\_theme.samples.Complex.imag -=========================================== - -.. currentmodule:: pyansys_sphinx_theme.samples - -.. autoproperty:: Complex.imag \ No newline at end of file diff --git a/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.real.rst b/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.real.rst deleted file mode 100644 index e81172e28..000000000 --- a/doc/source/documentation_style/api/pyansys_sphinx_theme.samples.Complex.real.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyansys\_sphinx\_theme.samples.Complex.real -=========================================== - -.. currentmodule:: pyansys_sphinx_theme.samples - -.. autoproperty:: Complex.real \ No newline at end of file diff --git a/doc/source/guidelines/index.rst b/doc/source/guidelines/index.rst index da05c335c..b6bbf53c6 100644 --- a/doc/source/guidelines/index.rst +++ b/doc/source/guidelines/index.rst @@ -1,18 +1,17 @@ -Guidelines +Guidelines and Best Practices ############################# -The purpose of this section is to highligh some common practices that -can be applied to the entire PyAnsys project in order to remain consistent. +This section describes and outlines several best practices that can be +applied to PyAnsys libaries for the purpose of creating effective and +efficient Python libraries to interface with Ansys products and +services. These guidelines demonstrate how applications and complex +services expose functionalities such as logging, data transfer, and +Application APIs. -One of the main objectives of PyAnsys libraries is to wrap (encapsulate) -data and methods within units of execution while hiding data or parameters -in protected variables. - -Those guidelines demonstrate how applications and complex services expose -functionalities such as logging, data transfer... +Table of Contents +----------------- .. toctree:: - :hidden: - :maxdepth: 3 + :maxdepth: 2 dev_practices app_interface_abstraction @@ -20,8 +19,3 @@ functionalities such as logging, data transfer... logging service_abstraction - -List of guidelines -------------------- - -* :ref:`ref_guide_logging`. Guidelines for logging in PyAnsys. diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index e61bfb165..ef9b23151 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -1,58 +1,84 @@ .. _ref_guide_logging: -Logging Guideline -################### +Logging Guidelines +################## -This page describes the general framework for logging in PyAnsys package and its libraries. +This section describes several guidelines for logging in PyAnsys +libraries. These guidelines are best practices discovered through +implementing logging services and modules within PyAnsys +libraries. Suggestions and improvements are welcome. -Logging in PyAnsys -=================== +Logging in PyAnsys Libraries +============================ -The logging capabilities in PyAnsys are built upon the `logging `__ library. -It does *NOT* intend to replace this library, rather provide a standardized way to interact between the built-in ``logging`` library and ``PyAnsys`` module. -There are two main loggers in PyAnsys, the *Global logger* and *Instance logger*. -These loggers are customized classes that wraps the ``Logger`` class from ``logging`` module and add specific features to it. +The logging capabilities in PyAnsys libraries should be built upon the +`standard logging `__ +library. PyAnsys libries should not to replace this library, rather provide +a standardized way to interact between the built-in :mod:`logging` +library and ``PyAnsys`` libraries. +Application or Service Logging +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The following guidelines describe "Application" or "Service" logging +from a PyAnsys library, where a PyAnsys library is used to extend or +expose features from an Ansys application, product, or service that +may be local or remote. + +There should be two main loggers in a PyAnsys library that exposes or +extends a service based application, the *Global logger* and *Instance +logger*. These loggers are customized classes that wrap +:class:`logging.Logger` from :mod:`logging` module and add specific +features to it. :ref:`logging_in_pymapdl_figure` outlines the logging +approach used by PyMAPDL and the scopes of the global and local +loggers. + +.. _logging_in_pymapdl_figure: + .. figure:: images/Guidelines_chart.png :align: center - :alt: Logging in PyAnsys + :alt: Logging in PyMAPDL :figclass: align-center - **Figure 1: Loggers structure in PyAnsys** + **Figure 1: Example Logging Structure in PyMAPDL** -Global logger -~~~~~~~~~~~~~~~~~ +Example Global logger +~~~~~~~~~~~~~~~~~~~~~ -There is a global logger named ``pymapdl_global`` which is created when importing ``ansys.mapdl.core`` (``ansys.mapdl.core.__init__``). -This logger is recommended for most scenarios, especially when the library ``pool`` is not involved, since it does not track the instances, rather the whole library. -If you intend to log the initialization of a library or module, you should use this logger. -If you want to use this global logger, you must import it at the top of your script or module: +There is a global logger named ``py*_global`` which is created when +importing ``ansys.product.service`` +(``ansys.product.service.__init__``). This logger is recommended for +most scenarios, especially when complex modules or classes are not +involved, since it does not track instances, rather can be used +globally. If you intend to log the initialization of a library or +module, you should use this logger. To use this global logger, you +must import it at the top of your script or module: .. code:: python - from ansys.mapdl.core import LOG + from ansys.product.service import LOG You could also rename it to avoid conflicts with other loggers (if any): .. code:: python - from ansys.mapdl.core import LOG as logger + from ansys.product.service import LOG as logger -It should be noticed that the default logging level of ``LOG`` is ``ERROR`` (``logging.ERROR``). -To change this and output different error level messages you can use the next approach: +It should be noted that the default logging level of ``LOG`` is +``ERROR`` (``logging.ERROR``). To change this and output different +ferror level messages you can use the next approach: .. code:: python LOG.logger.setLevel('DEBUG') - LOG.file_handler.setLevel('DEBUG') # If present. - LOG.stdout_handler.setLevel('DEBUG') # If present. + LOG.file_handler.setLevel('DEBUG') # if present + LOG.stdout_handler.setLevel('DEBUG') # if present -Alternatively, you can do: +Alternatively, you can use: .. code:: python @@ -61,20 +87,22 @@ Alternatively, you can do: This way ensures all the handlers are set to the desired log level. -By default, this logger does not log to a file. If you wish to do so, you can add a file handler using: +By default, this logger does not log to a file. If you wish to do so, +you can add a file handler using: .. code:: python import os - file_path = os.path.join(os.getcwd(), 'pymapdl.log') + file_path = os.path.join(os.getcwd(), 'pylibrary.log') LOG.log_to_file(file_path) +This enables logging to that file in addition of the standard output. +If you wish to change the characteristics of this global logger from +the beginning of the execution, you must edit the file ``__init__`` in +the directory of your library. -This sets the logger to be redirected also to that file, in addition of the standard output. -If you wish to change the characteristics of this global logger from the beginning of the execution, -you must edit the file ``__init__`` in the directory ``ansys.mapdl.core``. - -To log using this logger, just call the desired method as a normal logger. +To log using this logger, simply call the desired method as a normal +logger. .. code:: python @@ -82,30 +110,33 @@ To log using this logger, just call the desired method as a normal logger. >>> from ansys.mapdl.core.logging import Logger >>> LOG = Logger(level=logging.DEBUG, to_file=False, to_stdout=True) >>> LOG.debug('This is LOG debug message.') - | Level | Instance | Module | Function | Message |----------|-----------------|------------------|----------------------|-------------------------------------------------------- | DEBUG | | __init__ | | This is LOG debug message. - Instance logger -~~~~~~~~~~~~~~~~~ -Every time that the class ``_MapdlCore`` is instantiated, a logger is created. -This logger is recommended when using the ``pool`` library or when using multiple instances of ``Mapdl``. -The main feature of this logger is that it tracks each instance and it includes its name when logging. -The name of the instances are unique. -For example in case of using the ``gRPC`` ``Mapdl`` version, its name includes the IP and port of the correspondent instance, making unique its logger. +~~~~~~~~~~~~~~~ +Every time that the class ``_MapdlCore`` is instantiated, a logger is +created. This logger is recommended when using the ``pool`` library +or when using multiple instances of ``Mapdl``. The main feature of +this logger is that it tracks each instance and it includes its name +when logging. The name of the instances are unique. For example in +case of using the ``gRPC`` ``Mapdl`` version, its name includes the IP +and port of the correspondent instance, making unique its logger. The instance loggers can be accessed in two places: * ``_MapdlCore._log``. For backward compatibility. -* ``LOG._instances``. This field is a ``dict`` where the key is the name of the created logger. +* ``LOG._instances``. This field is a ``dict`` where the key is the + name of the created logger. -These instance loggers inherit from the ``pymapdl_global`` output handlers and logging level unless otherwise specified. -The way this logger works is very similar to the global logger. -You can add a file handler if you wish using the method ``log_to_file`` or change the log level using ``setLevel`` method. +These instance loggers inherit from the ``pymapdl_global`` output +handlers and logging level unless otherwise specified. The way this +logger works is very similar to the global logger. You can add a file +handler if you wish using the method ``log_to_file`` or change the log +level using :meth:`logging.Logger.setLevel`. You can use this logger like this: @@ -227,9 +258,13 @@ To do so, a boolean argument can be added in the initializer of the ``Logger`` c Formatting -~~~~~~ -Even if the current practice recommends using the f-string to format your strings, when it comes to logging, the former %-formatting suits the need. -This way the string is not constantly interpolated. It is deferred and evaluated only when the message is emitted. +~~~~~~~~~~ +Even if the current practice recommends using the f-string to format +your strings, when it comes to logging, the former %-formatting is +preferable. This way the string format is not evaluated at +runtime. It is deferred and evaluated only when the message is +emitted. If there is any formatting or evaluation error, these will be +reported as logging errors and will not halt code execution. .. code:: python @@ -238,8 +273,12 @@ This way the string is not constantly interpolated. It is deferred and evaluated Enable/Disable handlers ~~~~~~~~~~~~~~~~~~~~~~~ -Sometimes the customer might want to disable specific handlers such as a file handler in which log messages are written. -If so, the existing handler must be properly closed and removed. Otherwise the file access might be denied later when you try to write new log content. +Sometimes the user might want to disable specific handlers such as a +file handler where log messages are written. If so, the existing +handler must be properly closed and removed. Otherwise the file access +might be denied later when you try to write new log content. + +Here's one approach to closing log handlers. .. code:: python diff --git a/logging/README.md b/logging/README.md new file mode 100644 index 000000000..4bb356371 --- /dev/null +++ b/logging/README.md @@ -0,0 +1,5 @@ +#### Example Logging Modules + +These modules demonstrate one way of logging at the global and +instance level for a PyAnsys libary that exposes and extends an a +service based application. diff --git a/logging/pyansys_logging.py b/logging/pyansys_logging.py new file mode 100644 index 000000000..db2f54a44 --- /dev/null +++ b/logging/pyansys_logging.py @@ -0,0 +1,479 @@ +from copy import copy +from datetime import datetime +import logging +from logging import DEBUG, INFO, WARN, ERROR, CRITICAL +import sys + +# Default configuration +LOG_LEVEL = logging.DEBUG +FILE_NAME = "pyproject.log" + + +# Formatting +STDOUT_MSG_FORMAT = ( + "%(levelname)s - %(instance_name)s - %(module)s - %(funcName)s - %(message)s" +) +FILE_MSG_FORMAT = STDOUT_MSG_FORMAT + +DEFAULT_STDOUT_HEADER = """ +LEVEL - INSTANCE NAME - MODULE - FUNCTION - MESSAGE +""" +DEFAULT_FILE_HEADER = DEFAULT_STDOUT_HEADER + +NEW_SESSION_HEADER = f""" +=============================================================================== + NEW SESSION - {datetime.now().strftime("%m/%d/%Y, %H:%M:%S")} +===============================================================================""" + +string_to_loglevel = { + "DEBUG": DEBUG, + "INFO": INFO, + "WARN": WARN, + "WARNING": WARN, + "ERROR": ERROR, + "CRITICAL": CRITICAL, +} + + +class InstanceCustomAdapter(logging.LoggerAdapter): + """This is key to keep the reference to a product instance name dynamic. + + If we use the standard approach which is supplying ``extra`` input + to the logger, we would need to keep inputting product instances + every time a log is created. + + Using adapters we just need to specify the product instance we refer + to once. + """ + + # level is kept for compatibility with ``supress_logging``, + # but it does nothing. + level = None + file_handler = None + stdout_handler = None + + def __init__(self, logger, extra=None): + self.logger = logger + self.extra = extra + self.file_handler = logger.file_handler + self.std_out_handler = logger.std_out_handler + + def process(self, msg, kwargs): + kwargs["extra"] = {} + # These are the extra parameters sent to log + # here self.extra is the argument pass to the log records. + kwargs["extra"]["instance_name"] = self.extra.get_name() + return msg, kwargs + + def log_to_file(self, filename=FILE_NAME, level=LOG_LEVEL): + """Add file handler to logger. + + Parameters + ---------- + filename : str, optional + Name of the file where the logs are recorded. By default + ``PyProject.log`` + level : str, optional + Level of logging, for example ``'DEBUG'``. By default + ``logging.DEBUG``. + """ + + self.logger = add_file_handler( + self.logger, filename=filename, level=level, write_headers=True + ) + self.file_handler = self.logger.file_handler + + def log_to_stdout(self, level=LOG_LEVEL): + """Add standard output handler to the logger. + + Parameters + ---------- + level : str, optional + Level of logging record. By default ``logging.DEBUG``. + """ + if self.std_out_handler: + raise Exception("Stdout logger already defined.") + + self.logger = add_stdout_handler(self.logger, level=level) + self.std_out_handler = self.logger.std_out_handler + + def setLevel(self, level="DEBUG"): + """Change the log level of the object and the attached handlers.""" + self.logger.setLevel(level) + for each_handler in self.logger.handlers: + each_handler.setLevel(level) + self.level = level + + +class PyAnsysPercentStyle(logging.PercentStyle): + def __init__(self, fmt, *, defaults=None): + self._fmt = fmt or self.default_format + self._defaults = defaults + + def _format(self, record): + defaults = self._defaults + if defaults: + values = defaults | record.__dict__ + else: + values = record.__dict__ + + # Here we can make any changes we want in the record, for + # example adding a key. + + # We could create an if here if we want conditional formatting, and even + # change the record.__dict__. + # Since now we don't want to create conditional fields, it is fine to keep + # the same MSG_FORMAT for all of them. + + # For the case of logging exceptions to the logger. + values.setdefault("instance_name", "") + + return STDOUT_MSG_FORMAT % values + + +class PyProjectFormatter(logging.Formatter): + """Customized ``Formatter`` class used to overwrite the defaults format styles.""" + + def __init__( + self, + fmt=STDOUT_MSG_FORMAT, + datefmt=None, + style="%", + validate=True, + defaults=None, + ): + if sys.version_info[1] < 8: + super().__init__(fmt, datefmt, style) + else: + # 3.8: The validate parameter was added + super().__init__(fmt, datefmt, style, validate) + self._style = PyAnsysPercentStyle(fmt, defaults=defaults) # overwriting + + +class InstanceFilter(logging.Filter): + """Ensures that instance_name record always exists.""" + + def filter(self, record): + if not hasattr(record, "instance_name"): + record.instance_name = "" + return True + + +class Logger: + """Logger used for each PyProject session. + + This class allows you to add handler to a file or standard output. + + Parameters + ---------- + level : int, optional + Logging level to filter the message severity allowed in the logger. + The default is ``logging.DEBUG``. + to_filet : bool, optional + Write log messages to a file. The default is ``False``. + to_stdout : bool, optional + Write log messages into the standard output. The + default is ``True``. + filename : str, optional + Name of the file where log messages are written to. + The default is ``None``. + """ + + file_handler = None + std_out_handler = None + _level = logging.DEBUG + _instances = {} + + def __init__( + self, level=logging.DEBUG, to_file=False, to_stdout=True, filename=FILE_NAME + ): + """Customized logger class for PyProject. + + Parameters + ---------- + level : str, optional + Level of logging as defined in the package ``logging``. By + default ``'DEBUG'``. + to_file : bool, optional + To record the logs in a file, by default ``False``. + to_stdout : bool, optional + To output the logs to the standard output, which is the + command line. By default ``True``. + filename : str, optional + Name of the output file. By default ``"pyproject.log"``. + """ + + self.logger = logging.getLogger( + "pyproject_global" + ) # Creating default main logger. + self.logger.addFilter(InstanceFilter()) + self.logger.setLevel(level) + self.logger.propagate = True + self.level = self.logger.level # TODO: TO REMOVE + + # Writing logging methods. + self.debug = self.logger.debug + self.info = self.logger.info + self.warning = self.logger.warning + self.error = self.logger.error + self.critical = self.logger.critical + self.log = self.logger.log + + if to_file or filename != FILE_NAME: + # We record to file + self.log_to_file(filename=filename, level=level) + + if to_stdout: + self.log_to_stdout(level=level) + + self.add_handling_uncaught_expections( + self.logger + ) # Using logger to record unhandled exceptions + + def log_to_file(self, filename=FILE_NAME, level=LOG_LEVEL): + """Add file handler to logger. + + Parameters + ---------- + filename : str, optional + Name of the file where the logs are recorded. By default FILE_NAME + level : str, optional + Level of logging. E.x. 'DEBUG'. By default LOG_LEVEL + """ + + self = add_file_handler( + self, filename=filename, level=level, write_headers=True + ) + + def log_to_stdout(self, level=LOG_LEVEL): + """Add standard output handler to the logger. + + Parameters + ---------- + level : str, optional + Level of logging record. By default LOG_LEVEL + """ + + self = add_stdout_handler(self, level=level) + + def setLevel(self, level="DEBUG"): + """Change the log level of the object and the attached handlers.""" + self.logger.setLevel(level) + for each_handler in self.logger.handlers: + each_handler.setLevel(level) + self._level = level + + def _make_child_logger(self, sufix, level): + """Create a child logger. + + Create a child logger either using ``getChild`` or copying + attributes between ``pyproject_global`` logger and the new + one. + + """ + logger = logging.getLogger(sufix) + logger.std_out_handler = None + logger.file_handler = None + + if self.logger.hasHandlers: + for each_handler in self.logger.handlers: + new_handler = copy(each_handler) + + if each_handler == self.file_handler: + logger.file_handler = new_handler + elif each_handler == self.std_out_handler: + logger.std_out_handler = new_handler + + if level: + # The logger handlers are copied and changed the + # loglevel is the specified log level is lower + # than the one of the global. + if each_handler.level > string_to_loglevel[level.upper()]: + new_handler.setLevel(level) + + logger.addHandler(new_handler) + + if level: + if isinstance(level, str): + level = string_to_loglevel[level.upper()] + logger.setLevel(level) + + else: + logger.setLevel(self.logger.level) + + logger.propagate = True + return logger + + def add_child_logger(self, sufix, level=None): + """Add a child logger to the main logger. + + This logger is more general than an instance logger which is designed + to track the state of the application instances. + + If the logging level is in the arguments, a new logger with a + reference to the ``_global`` logger handlers is created + instead of a child. + + Parameters + ---------- + sufix : str + Name of the logger. + level : str + Level of logging + + Returns + ------- + logging.logger + Logger class. + """ + name = self.logger.name + "." + sufix + self._instances[name] = self._make_child_logger(self, name, level) + return self._instances[name] + + def _add_product_instance_logger(self, name, product_instance, level): + if isinstance(name, str): + instance_logger = InstanceCustomAdapter( + self._make_child_logger(name, level), product_instance + ) + elif isinstance(name, None): + instance_logger = InstanceCustomAdapter( + self._make_child_logger("NO_NAMED_YET", level), product_instance + ) + else: + raise TypeError( + f"``name`` parameter must be a string or None, not f{type(name)}" + ) + + return instance_logger + + def add_instance_logger(self, name, product_instance, level=None): + """Create a logger for an application instance. + + This instance logger is a logger with an adapter which add the + contextual information such as instance + name. This logger is returned and you can use it to log events + as a normal logger. It is also stored in the ``_instances`` + attribute. + + Parameters + ---------- + name : str + Name for the new logger + product_instance : ansys.product.service.module.ProductClass + Class instance. This must contain the attribute ``name``. + + Returns + ------- + InstanceCustomAdapter + Logger adapter customized to add additional information to + the logs. You can use this class to log events in the + same way you would with a logger class. + + Raises + ------ + TypeError + You can only input strings as ``name`` to this method. + """ + count_ = 0 + new_name = name + while new_name in logging.root.manager.__dict__.keys(): + count_ += 1 + new_name = name + "_" + str(count_) + + self._instances[new_name] = self._add_product_instance_logger( + new_name, product_instance, level + ) + return self._instances[new_name] + + def __getitem__(self, key): + if key in self._instances.keys(): + return self._instances[key] + else: + raise KeyError(f"There are no instances with name {key}") + + def add_handling_uncaught_expections(self, logger): + """This just redirects the output of an exception to the logger.""" + + def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + logger.critical( + "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback) + ) + + sys.excepthook = handle_exception + + +def add_file_handler(logger, filename=FILE_NAME, level=LOG_LEVEL, write_headers=False): + """Add a file handler to the input. + + Parameters + ---------- + logger : logging.Logger or logging.Logger + Logger where to add the file handler. + filename : str, optional + Name of the output file. By default FILE_NAME + level : str, optional + Level of log recording. By default LOG_LEVEL + write_headers : bool, optional + Record the headers to the file. By default ``False``. + + Returns + ------- + logger + Return the logger or Logger object. + """ + + file_handler = logging.FileHandler(filename) + file_handler.setLevel(level) + file_handler.setFormatter(logging.Formatter(FILE_MSG_FORMAT)) + + if isinstance(logger, Logger): + logger.file_handler = file_handler + logger.logger.addHandler(file_handler) + + elif isinstance(logger, logging.Logger): + logger.file_handler = file_handler + logger.addHandler(file_handler) + + if write_headers: + file_handler.stream.write(NEW_SESSION_HEADER) + file_handler.stream.write(DEFAULT_FILE_HEADER) + + return logger + + +def add_stdout_handler(logger, level=LOG_LEVEL, write_headers=False): + """Add a file handler to the logger. + + Parameters + ---------- + logger : logging.Logger or logging.Logger + Logger where to add the file handler. + level : str, optional + Level of log recording. By default ``logging.DEBUG``. + write_headers : bool, optional + Record the headers to the file. By default ``False``. + + Returns + ------- + logger + The logger or Logger object. + """ + + std_out_handler = logging.StreamHandler() + std_out_handler.setLevel(level) + std_out_handler.setFormatter(PyProjectFormatter(STDOUT_MSG_FORMAT)) + + if isinstance(logger, Logger): + logger.std_out_handler = std_out_handler + logger.logger.addHandler(std_out_handler) + + elif isinstance(logger, logging.Logger): + logger.addHandler(std_out_handler) + + if write_headers: + std_out_handler.stream.write(DEFAULT_STDOUT_HEADER) + + return logger diff --git a/requirements_docs.txt b/requirements_docs.txt index 8b485d918..709d1f95e 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,2 +1,3 @@ -pyansys-sphinx-theme -Sphinx==4.0.3 # using this version due to the link to the logo +Sphinx==4.3.2 +pyansys-sphinx-theme==0.2.0 +sphinx-copybutton==0.4.0 diff --git a/requirements_style.txt b/requirements_style.txt index f249615f6..f85124708 100644 --- a/requirements_style.txt +++ b/requirements_style.txt @@ -1,3 +1,3 @@ -codespell==2.0.0 +codespell==2.1.0 From 55a6d452d7e09aac97b4e36d5ec64c96b48d6b76 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 30 Dec 2021 21:04:45 -0700 Subject: [PATCH 09/32] ignore gitignore --- .gitignore | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8b11f3df6..c6cefb63b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,12 @@ dist/* ######################### *.egg-info/ - +# Build docs and packages build/* doc/build/ # virtual environment -venv/ \ No newline at end of file +venv/ + +# sphinx autogen +doc/source/documentation_style/api/* \ No newline at end of file From c30fa838f5a13c53a01575a7613b444b533da1e3 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Fri, 31 Dec 2021 10:54:40 +0100 Subject: [PATCH 10/32] Fix typo. --- doc/source/guidelines/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/guidelines/index.rst b/doc/source/guidelines/index.rst index b6bbf53c6..51030f4b2 100644 --- a/doc/source/guidelines/index.rst +++ b/doc/source/guidelines/index.rst @@ -3,7 +3,7 @@ Guidelines and Best Practices This section describes and outlines several best practices that can be applied to PyAnsys libaries for the purpose of creating effective and efficient Python libraries to interface with Ansys products and -services. These guidelines demonstrate how applications and complex +services. These guidelines demonstrate how applications and complex services expose functionalities such as logging, data transfer, and Application APIs. From 79cbbc44ef1b75ffe39241cead4b585e9f3c0219 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Fri, 31 Dec 2021 10:55:23 +0100 Subject: [PATCH 11/32] Why and when to log. --- doc/source/guidelines/logging.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index ef9b23151..26bdb7bdc 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -1,5 +1,3 @@ -.. _ref_guide_logging: - Logging Guidelines ################## @@ -9,6 +7,22 @@ implementing logging services and modules within PyAnsys libraries. Suggestions and improvements are welcome. +Why and when to log +=================== +Logging helps to track events occurring in the application. +It is destinated to both the users and the application developers. +It can serve several purposes: + + - extract some valuable data for the final users to know the status of their work. + - track the progress and the course of the application usage. + - provide the developer with as much information as possible if an issue happens. + +The message logged can contain generic information or embed data specific to the current session. +Message content is associated to a level of severity (info, warning, error...). +Generally, this degree of significance indicates the recipient of the message. +An info message is directed to the user while a debug message is useful for the developer itself. + + Logging in PyAnsys Libraries ============================ From 3576281dbffff66ae0ae4393916b481786439ce3 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Fri, 31 Dec 2021 11:02:42 +0100 Subject: [PATCH 12/32] Always log to share data. --- doc/source/guidelines/logging.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 26bdb7bdc..8eef1f47a 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -9,7 +9,9 @@ libraries. Suggestions and improvements are welcome. Why and when to log =================== -Logging helps to track events occurring in the application. +Logging helps to track events occurring in the application. +Whenever an information must be exposed, displayed and shared, logging is the +way to do it. It is destinated to both the users and the application developers. It can serve several purposes: @@ -17,10 +19,12 @@ It can serve several purposes: - track the progress and the course of the application usage. - provide the developer with as much information as possible if an issue happens. -The message logged can contain generic information or embed data specific to the current session. +The message logged can contain generic information or embed data specific +to the current session. Message content is associated to a level of severity (info, warning, error...). Generally, this degree of significance indicates the recipient of the message. -An info message is directed to the user while a debug message is useful for the developer itself. +An info message is directed to the user while a debug message is useful for +the developer itself. Logging in PyAnsys Libraries From e988cbdeaa592d72ad7af669c9bb311f263a0831 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Fri, 31 Dec 2021 18:41:01 +0100 Subject: [PATCH 13/32] Add sphinx_toolbox in the doc requirements. --- requirements_docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_docs.txt b/requirements_docs.txt index 709d1f95e..352bef49b 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,4 @@ Sphinx==4.3.2 pyansys-sphinx-theme==0.2.0 sphinx-copybutton==0.4.0 +sphinx_toolbox From c7a62c3e730599a1159537236154c9cc21eb184f Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 16:35:39 +0100 Subject: [PATCH 14/32] Improve some comments in the logging example. --- logging/pyansys_logging.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/logging/pyansys_logging.py b/logging/pyansys_logging.py index db2f54a44..5889d9ac4 100644 --- a/logging/pyansys_logging.py +++ b/logging/pyansys_logging.py @@ -6,12 +6,12 @@ # Default configuration LOG_LEVEL = logging.DEBUG -FILE_NAME = "pyproject.log" +FILE_NAME = "PyProject.log" # Formatting STDOUT_MSG_FORMAT = ( - "%(levelname)s - %(instance_name)s - %(module)s - %(funcName)s - %(message)s" + "%(levelname)s - %(instance_name)s - %(module)s - %(funcName)s - %(message)s" ) FILE_MSG_FORMAT = STDOUT_MSG_FORMAT @@ -46,7 +46,7 @@ class InstanceCustomAdapter(logging.LoggerAdapter): to once. """ - # level is kept for compatibility with ``supress_logging``, + # level is kept for compatibility with ``suppress_logging``, # but it does nothing. level = None file_handler = None @@ -162,14 +162,14 @@ def filter(self, record): class Logger: """Logger used for each PyProject session. - This class allows you to add handler to a file or standard output. + This class allows you to add a handler to a file or standard output. Parameters ---------- level : int, optional Logging level to filter the message severity allowed in the logger. The default is ``logging.DEBUG``. - to_filet : bool, optional + to_file : bool, optional Write log messages to a file. The default is ``False``. to_stdout : bool, optional Write log messages into the standard output. The @@ -200,7 +200,7 @@ def __init__( To output the logs to the standard output, which is the command line. By default ``True``. filename : str, optional - Name of the output file. By default ``"pyproject.log"``. + Name of the output file. By default ``"PyProject.log"``. """ self.logger = logging.getLogger( @@ -220,7 +220,7 @@ def __init__( self.log = self.logger.log if to_file or filename != FILE_NAME: - # We record to file + # We record to file. self.log_to_file(filename=filename, level=level) if to_stdout: @@ -228,7 +228,7 @@ def __init__( self.add_handling_uncaught_expections( self.logger - ) # Using logger to record unhandled exceptions + ) # Using logger to record unhandled exceptions. def log_to_file(self, filename=FILE_NAME, level=LOG_LEVEL): """Add file handler to logger. @@ -286,7 +286,7 @@ def _make_child_logger(self, sufix, level): if level: # The logger handlers are copied and changed the - # loglevel is the specified log level is lower + # loglevel if the specified log level is lower # than the one of the global. if each_handler.level > string_to_loglevel[level.upper()]: new_handler.setLevel(level) @@ -366,7 +366,7 @@ def add_instance_logger(self, name, product_instance, level=None): ------- InstanceCustomAdapter Logger adapter customized to add additional information to - the logs. You can use this class to log events in the + the logs. You can use this class to log events in the same way you would with a logger class. Raises @@ -462,7 +462,7 @@ def add_stdout_handler(logger, level=LOG_LEVEL, write_headers=False): The logger or Logger object. """ - std_out_handler = logging.StreamHandler() + std_out_handler = logging.StreamHandler(sys.stdout) std_out_handler.setLevel(level) std_out_handler.setFormatter(PyProjectFormatter(STDOUT_MSG_FORMAT)) From ed4c5808fc8b8871b641c91551716c9f3f6fd56e Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 16:39:20 +0100 Subject: [PATCH 15/32] Replace 'file' by 'stdout' for the stream. --- logging/pyansys_logging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logging/pyansys_logging.py b/logging/pyansys_logging.py index 5889d9ac4..d83f9a756 100644 --- a/logging/pyansys_logging.py +++ b/logging/pyansys_logging.py @@ -445,16 +445,16 @@ def add_file_handler(logger, filename=FILE_NAME, level=LOG_LEVEL, write_headers= def add_stdout_handler(logger, level=LOG_LEVEL, write_headers=False): - """Add a file handler to the logger. + """Add a stream handler to the logger. Parameters ---------- logger : logging.Logger or logging.Logger - Logger where to add the file handler. + Logger where to add the stream handler. level : str, optional Level of log recording. By default ``logging.DEBUG``. write_headers : bool, optional - Record the headers to the file. By default ``False``. + Record the headers to the stream. By default ``False``. Returns ------- From 07152b9a3f4e254da994ebbe4e35b0f2a7cf1dad Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 17:04:55 +0100 Subject: [PATCH 16/32] Add unit tests for the logging example module. --- tests/test_pyansys_logging.py | 141 ++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/test_pyansys_logging.py diff --git a/tests/test_pyansys_logging.py b/tests/test_pyansys_logging.py new file mode 100644 index 000000000..1e6744c0a --- /dev/null +++ b/tests/test_pyansys_logging.py @@ -0,0 +1,141 @@ +import io +import logging +import os +import sys +import tempfile + +import pytest + +sys.path.append(os.path.join("..", "logging")) +import pyansys_logging + + +def test_default_logger(): + """Create a logger with default options. + Only stdout logger must be used.""" + + capture = CaptureStdOut() + with capture: + test_logger = pyansys_logging.Logger() + test_logger.info("Test stdout") + + assert "INFO - - test_pyansys_logging - test_default_logger - Test stdout" in capture.content + # File handlers are not activated. + assert os.path.exists(os.path.exists(os.path.join(os.getcwd(), "PyProject.log"))) + + +def test_level_stdout(): + """Create a logger with default options. + Only stdout logger must be used.""" + + capture = CaptureStdOut() + with capture: + test_logger = pyansys_logging.Logger(level=logging.INFO) + test_logger.debug("Debug stdout with level=INFO") + test_logger.info("Info stdout with level=INFO") + test_logger.warning("Warning stdout with level=INFO") + test_logger.error("Error stdout with level=INFO") + test_logger.critical("Critical stdout with level=INFO") + + # Modify the level + test_logger.setLevel(level=logging.WARNING) + test_logger.debug("Debug stdout with level=WARNING") + test_logger.info("Info stdout with level=WARNING") + test_logger.warning("Warning stdout with level=WARNING") + test_logger.error("Error stdout with level=WARNING") + test_logger.critical("Critical stdout with level=WARNING") + + # level=INFO + assert "DEBUG - - test_pyansys_logging - test_level_stdout - Debug stdout with level=INFO" not in capture.content + assert "INFO - - test_pyansys_logging - test_level_stdout - Info stdout with level=INFO" in capture.content + assert "WARNING - - test_pyansys_logging - test_level_stdout - Warning stdout with level=INFO" in capture.content + assert "ERROR - - test_pyansys_logging - test_level_stdout - Error stdout with level=INFO" in capture.content + assert "CRITICAL - - test_pyansys_logging - test_level_stdout - Critical stdout with level=INFO" in capture.content + # level=WARNING + assert "INFO - - test_pyansys_logging - test_level_stdout - Info stdout with level=WARNING" not in capture.content + assert ( + "WARNING - - test_pyansys_logging - test_level_stdout - Warning stdout with level=WARNING" in capture.content + ) + assert "ERROR - - test_pyansys_logging - test_level_stdout - Error stdout with level=WARNING" in capture.content + assert ( + "CRITICAL - - test_pyansys_logging - test_level_stdout - Critical stdout with level=WARNING" in capture.content + ) + + # File handlers are not activated. + assert os.path.exists(os.path.exists(os.path.join(os.getcwd(), "PyProject.log"))) + + +def test_default_file_handlers(): + """Activate the `PyProject.log` file handler.""" + + current_dirctory = os.getcwd() + file_logger = os.path.join(current_dirctory, "PyProject.log") + if os.path.exists(file_logger): + os.remove(file_logger) + + content = None + test_logger = pyansys_logging.Logger(to_file=True) + test_logger.info("Test PyProject.Log") + + with open(file_logger, "r") as f: + content = f.readlines() + + assert len(content) == 6 + assert "NEW SESSION" in content[2] + assert "===============================================================================" in content[3] + assert "LEVEL - INSTANCE NAME - MODULE - FUNCTION - MESSAGE" in content[4] + assert "INFO - - test_pyansys_logging - test_default_file_handlers - Test PyProject.Log" in content[5] + + # Remove file's handlers and delete the file. + for handler in test_logger.logger.handlers: + if isinstance(handler, logging.FileHandler): + handler.close() + test_logger.logger.removeHandler(handler) + os.remove(file_logger) + + +def test_file_handlers(): + """Activate a file handler different from `PyProject.log`.""" + + content = None + current_dirctory = os.getcwd() + file_logger = os.path.join(current_dirctory, "test_logger.txt") + if os.path.exists(file_logger): + os.remove(file_logger) + test_logger = pyansys_logging.Logger(to_file=True, filename=file_logger) + test_logger.info("Test Misc File") + + with open(file_logger, "r") as f: + content = f.readlines() + + assert os.path.exists(file_logger) # The file handler is not the default PyProject.Log + assert len(content) == 6 + assert "NEW SESSION" in content[2] + assert "===============================================================================" in content[3] + assert "LEVEL - INSTANCE NAME - MODULE - FUNCTION - MESSAGE" in content[4] + assert "INFO - - test_pyansys_logging - test_file_handlers - Test Misc File" in content[5] + + # Remove file's handlers and delete the file. + for handler in test_logger.logger.handlers: + if isinstance(handler, logging.FileHandler): + handler.close() + test_logger.logger.removeHandler(handler) + os.remove(file_logger) + + +class CaptureStdOut: + """Capture standard output with a context manager.""" + + def __init__(self): + self._stream = io.StringIO() + + def __enter__(self): + sys.stdout = self._stream + + def __exit__(self, type, value, traceback): + sys.stdout = sys.__stdout__ + + @property + def content(self): + """Return the captured content.""" + return self._stream.getvalue() From ae7ef10ec19c998c4dc8884bd83b81476755adaf Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Mon, 3 Jan 2022 11:48:03 -0700 Subject: [PATCH 17/32] add literalinclude --- doc/source/conf.py | 1 + doc/source/guidelines/logging.rst | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 9cffd61f8..cb5709c9c 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -19,6 +19,7 @@ # Sphinx extensions extensions = [ "sphinx_copybutton", + 'sphinx_toolbox.collapse', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index ef9b23151..6975145ab 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -22,13 +22,13 @@ library and ``PyAnsys`` libraries. Application or Service Logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following guidelines describe "Application" or "Service" logging -from a PyAnsys library, where a PyAnsys library is used to extend or -expose features from an Ansys application, product, or service that -may be local or remote. +module for a PyAnsys library, where a PyAnsys library is used to +extend or expose features from an Ansys application, product, or +service that may be local or remote. -There should be two main loggers in a PyAnsys library that exposes or -extends a service based application, the *Global logger* and *Instance -logger*. These loggers are customized classes that wrap +This section describes two two main loggers for a PyAnsys library that +exposes or extends a service based application, the *Global logger* +and the *Instance logger*. These loggers are customized classes that wrap :class:`logging.Logger` from :mod:`logging` module and add specific features to it. :ref:`logging_in_pymapdl_figure` outlines the logging approach used by PyMAPDL and the scopes of the global and local @@ -43,6 +43,15 @@ loggers. **Figure 1: Example Logging Structure in PyMAPDL** +The source for this example logger can be found both within developers +guide repository at `pyansys_logging.py +`_ +as well as below in the collapsable section below: + +.. collapse:: Example PyAnsys custom logger module + + .. literalinclude:: ../../../logging/pyansys_logging.py + Example Global logger ~~~~~~~~~~~~~~~~~~~~~ From c699453dd98b4ab8ad78fa4e8ce0ca4693ab8a87 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 20:13:25 +0100 Subject: [PATCH 18/32] Improve syntax. --- doc/source/guidelines/logging.rst | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 8eef1f47a..7b5f942a1 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -5,11 +5,14 @@ This section describes several guidelines for logging in PyAnsys libraries. These guidelines are best practices discovered through implementing logging services and modules within PyAnsys libraries. Suggestions and improvements are welcome. +External resources also describe `basic `__ +and `advanced `__ technics. -Why and when to log -=================== -Logging helps to track events occurring in the application. +Description and usage +===================== +Logging helps to track events occurring in the application. For each of them a log record +is created. It contains a detailed set of information about the current application operation. Whenever an information must be exposed, displayed and shared, logging is the way to do it. It is destinated to both the users and the application developers. @@ -172,12 +175,16 @@ You can use this logger like this: Other loggers ~~~~~~~~~~~~~~~~~ -You can create your own loggers using python ``logging`` library as you would do in any other script. -There shall be no conflicts between these. +A product, due to its architecture can be made of several loggers. +The ``logging`` library features allows to work a finite number of loggers. +The factory function logging.getLogger() helps to access each logger by its name. +In addition of this naming-mappings, a hierachy can be established to structure the loggers +order. For instance, if an ANSYS product is using a custom logger encapsulated inside the product itself, you might benefit from exposing it through the standard python tools. -It is recommended to use the standard library as much as possible. It will benefit every contributor to your project by exposing common tools that are widely spread. Each developer will be able to operate quickly and autonomously. +It is recommended to use the standard library as much as possible. It will benefit every contributor to your project by exposing common tools that are widely spread. +Each developer will be able to operate quickly and autonomously. Your project will take advantage of the entire set of features exposed in the standard logger and all the upcoming improvements. Create a custom log handler to catch each product message and redirect them on another logger: @@ -275,8 +282,8 @@ To do so, a boolean argument can be added in the initializer of the ``Logger`` c self.global_logger.addHandler(self._std_out_handler) -Formatting -~~~~~~~~~~ +String format +~~~~~~~~~~~~~ Even if the current practice recommends using the f-string to format your strings, when it comes to logging, the former %-formatting is preferable. This way the string format is not evaluated at From 9e3159926986f1f6735377443e1fe4f4f01eccf2 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 20:16:44 +0100 Subject: [PATCH 19/32] Move the test close to the module itself. --- {tests => logging}/test_pyansys_logging.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests => logging}/test_pyansys_logging.py (100%) diff --git a/tests/test_pyansys_logging.py b/logging/test_pyansys_logging.py similarity index 100% rename from tests/test_pyansys_logging.py rename to logging/test_pyansys_logging.py From 653567fd308af66428836b1eb997b68f9125030c Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 20:26:14 +0100 Subject: [PATCH 20/32] Add a best practice section. --- doc/source/guidelines/logging.rst | 83 +++++++++++++++++-------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 727f6354d..78700413e 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -40,6 +40,48 @@ a standardized way to interact between the built-in :mod:`logging` library and ``PyAnsys`` libraries. +Logging Best Practices +---------------------- + +Avoid printing to the console +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A common habit while prototyping a new feature is to print message into the command line executable. +Instead of using the common ``Print()`` method, it is advised to use a ``StreamHandler`` and redirect its content. +Indeed that will allow to filter messages based on their level and apply properly the formatter. +To do so, a boolean argument can be added in the initializer of the ``Logger`` class. +This argument specifies how to handle the stream. + +Enable/Disable handlers +~~~~~~~~~~~~~~~~~~~~~~~ +Sometimes the user might want to disable specific handlers such as a +file handler where log messages are written. If so, the existing +handler must be properly closed and removed. Otherwise the file access +might be denied later when you try to write new log content. + +Here's one approach to closing log handlers. + +.. code:: python + + for handler in design_logger.handlers: + if isinstance(handler, logging.FileHandler): + handler.close() + design_logger.removeHandler(handler) + + +String format +~~~~~~~~~~~~~ +Even if the current practice recommends using the f-string to format +your strings, when it comes to logging, the former %-formatting is +preferable. This way the string format is not evaluated at +runtime. It is deferred and evaluated only when the message is +emitted. If there is any formatting or evaluation error, these will be +reported as logging errors and will not halt code execution. + +.. code:: python + + logger.info("Project %s has been opened.", project.GetName()) + + Application or Service Logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following guidelines describe "Application" or "Service" logging @@ -73,6 +115,11 @@ as well as below in the collapsable section below: .. literalinclude:: ../../../logging/pyansys_logging.py +Following are some unit tests demonstatring how to use the code implemented above: + +.. collapse:: How to use PyAnsys custom logger module + + .. literalinclude:: ../../../logging/test_pyansys_logging.py Example Global logger ~~~~~~~~~~~~~~~~~~~~~ @@ -271,12 +318,6 @@ You must implement the ``filter`` method. It will contain all the modified conte record.extra = self._extra + ":" return True -Avoid printing to the console -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A common habit while prototyping a new feature is to print message into the command line executable. -Instead of using the common ``Print()`` method, it is advised to use a ``StreamHandler`` and redirect its content. -Indeed that will allow to filter messages based on their level and apply properly the formatter. -To do so, a boolean argument can be added in the initializer of the ``Logger`` class. This argument specifies how to handle the stream. .. code:: python @@ -290,33 +331,3 @@ To do so, a boolean argument can be added in the initializer of the ``Logger`` c self._std_out_handler.setFormatter(FORMATTER) self.global_logger.addHandler(self._std_out_handler) - -String format -~~~~~~~~~~~~~ -Even if the current practice recommends using the f-string to format -your strings, when it comes to logging, the former %-formatting is -preferable. This way the string format is not evaluated at -runtime. It is deferred and evaluated only when the message is -emitted. If there is any formatting or evaluation error, these will be -reported as logging errors and will not halt code execution. - -.. code:: python - - logger.info("Project %s has been opened.", project.GetName()) - - -Enable/Disable handlers -~~~~~~~~~~~~~~~~~~~~~~~ -Sometimes the user might want to disable specific handlers such as a -file handler where log messages are written. If so, the existing -handler must be properly closed and removed. Otherwise the file access -might be denied later when you try to write new log content. - -Here's one approach to closing log handlers. - -.. code:: python - - for handler in design_logger.handlers: - if isinstance(handler, logging.FileHandler): - handler.close() - design_logger.removeHandler(handler) From d6792c5e5733b6e2c975393b3be96ee79551bacb Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 20:29:40 +0100 Subject: [PATCH 21/32] Move the App filter under the best practices. --- doc/source/guidelines/logging.rst | 84 ++++++++++++++++--------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 78700413e..6473f407b 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -68,6 +68,47 @@ Here's one approach to closing log handlers. design_logger.removeHandler(handler) +App Filter +~~~~~~~~~~ +In case you need to modify the content of some messages you can apply filters. +This can be useful to harmonize the message rendering especially when you write in an external file. +To do so you can create a class based on the logging.Filter. +You must implement the ``filter`` method. It will contain all the modified content send to the stream. + +.. code:: python + + class AppFilter(logging.Filter): + + def __init__(self, destination="Global", extra=""): + self._destination = destination + self._extra = extra + + def filter(self, record): + """Modify the record sent to the stream."""" + + record.destination = self._destination + + # This will avoid the extra '::' for Global that does not have any extra info. + if not self._extra: + record.extra = self._extra + else: + record.extra = self._extra + ":" + return True + + +.. code:: python + + class CustomLogger(object): + + def __init__(self, messenger, level=logging.DEBUG, to_stdout=False): + + if to_stdout: + self._std_out_handler = logging.StreamHandler() + self._std_out_handler.setLevel(level) + self._std_out_handler.setFormatter(FORMATTER) + self.global_logger.addHandler(self._std_out_handler) + + String format ~~~~~~~~~~~~~ Even if the current practice recommends using the f-string to format @@ -229,8 +270,8 @@ You can use this logger like this: -Other loggers -~~~~~~~~~~~~~~~~~ +Wrapping Other Loggers +~~~~~~~~~~~~~~~~~~~~~~ A product, due to its architecture can be made of several loggers. The ``logging`` library features allows to work a finite number of loggers. The factory function logging.getLogger() helps to access each logger by its name. @@ -292,42 +333,3 @@ As a reminder the record is an object containing all kind of information related This custom handler is used into the new logger instance (the one based on the standard library). A good practice before to add a handler on any logger is to verify if any appropriate handler is already available in order to avoid any conflict, message duplication... - -App Filter -~~~~~~~~~~ -In case you need to modify the content of some messages you can apply filters. This can be useful to harmonize the message rendering especially when you write in an external file. To do so you can create a class based on the logging.Filter. -You must implement the ``filter`` method. It will contain all the modified content send to the stream. - -.. code:: python - - class AppFilter(logging.Filter): - - def __init__(self, destination="Global", extra=""): - self._destination = destination - self._extra = extra - - def filter(self, record): - """Modify the record sent to the stream."""" - - record.destination = self._destination - - # This will avoid the extra '::' for Global that does not have any extra info. - if not self._extra: - record.extra = self._extra - else: - record.extra = self._extra + ":" - return True - - -.. code:: python - - class CustomLogger(object): - - def __init__(self, messenger, level=logging.DEBUG, to_stdout=False): - - if to_stdout: - self._std_out_handler = logging.StreamHandler() - self._std_out_handler.setLevel(level) - self._std_out_handler.setFormatter(FORMATTER) - self.global_logger.addHandler(self._std_out_handler) - From 17b09ebe11930e45ca49d4f661fd55549d9beee9 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 20:53:33 +0100 Subject: [PATCH 22/32] Improve the app filter text. --- doc/source/guidelines/logging.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 6473f407b..6a6b2dd0c 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -70,10 +70,12 @@ Here's one approach to closing log handlers. App Filter ~~~~~~~~~~ -In case you need to modify the content of some messages you can apply filters. -This can be useful to harmonize the message rendering especially when you write in an external file. -To do so you can create a class based on the logging.Filter. -You must implement the ``filter`` method. It will contain all the modified content send to the stream. +A filter shows all its value when the content of a message depends on some conditions. +It injects contextual information in the core of the message. +This can be useful to harmonize the message rendering when the application output is not consistent +and vary upon the data processed. +It requires the creation of class based on the logging.Filter and the implementation of +the ``filter`` method. This method will contain all the modified content send to the stream. .. code:: python @@ -112,7 +114,7 @@ You must implement the ``filter`` method. It will contain all the modified conte String format ~~~~~~~~~~~~~ Even if the current practice recommends using the f-string to format -your strings, when it comes to logging, the former %-formatting is +most strings, when it comes to logging, the former %-formatting is preferable. This way the string format is not evaluated at runtime. It is deferred and evaluated only when the message is emitted. If there is any formatting or evaluation error, these will be From 9be5d391cbe826a1b5cac302c5401583e1ce9de4 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 20:55:36 +0100 Subject: [PATCH 23/32] Remove the sys.path.append(). --- logging/test_pyansys_logging.py | 1 - 1 file changed, 1 deletion(-) diff --git a/logging/test_pyansys_logging.py b/logging/test_pyansys_logging.py index 1e6744c0a..f57a1a140 100644 --- a/logging/test_pyansys_logging.py +++ b/logging/test_pyansys_logging.py @@ -6,7 +6,6 @@ import pytest -sys.path.append(os.path.join("..", "logging")) import pyansys_logging From 10de8df58d88e4b9440152ba9ba69e28ab9da993 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Mon, 3 Jan 2022 21:02:41 +0100 Subject: [PATCH 24/32] Solve title conflict. --- doc/source/guidelines/logging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 6a6b2dd0c..2479e4b49 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -290,7 +290,7 @@ Create a custom log handler to catch each product message and redirect them on a ============================================================================================== Context: -~~~~~~~~~ +-------- AEDT product has its own internal logger called the message manager made of 3 main destinations: From aea164090f22d4e420531d63d8ee71450b3f27d2 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Tue, 4 Jan 2022 10:02:07 +0100 Subject: [PATCH 25/32] Add Logger destructeur. --- logging/pyansys_logging.py | 19 +++++++++++++++++++ logging/test_pyansys_logging.py | 23 ++++++++++------------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/logging/pyansys_logging.py b/logging/pyansys_logging.py index d83f9a756..383442e29 100644 --- a/logging/pyansys_logging.py +++ b/logging/pyansys_logging.py @@ -230,6 +230,8 @@ def __init__( self.logger ) # Using logger to record unhandled exceptions. + self._cleanup = True + def log_to_file(self, filename=FILE_NAME, level=LOG_LEVEL): """Add file handler to logger. @@ -404,6 +406,23 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.excepthook = handle_exception + def __del__(self): + """Close the logger and all its handlers.""" + self.logger.debug("Collecting logger") + if self._cleanup: + try: + for handler in self.logger.handlers: + handler.close() + self.logger.removeHandler(handler) + except Exception as e: + try: + if self.logger is not None: + self.logger.error("The logger was not deleted properly.") + except Exception: + pass + else: + self.logger.debug("Collecting but not exiting due to 'self._cleanup = False'") + def add_file_handler(logger, filename=FILE_NAME, level=LOG_LEVEL, write_headers=False): """Add a file handler to the input. diff --git a/logging/test_pyansys_logging.py b/logging/test_pyansys_logging.py index f57a1a140..269d23222 100644 --- a/logging/test_pyansys_logging.py +++ b/logging/test_pyansys_logging.py @@ -2,9 +2,7 @@ import logging import os import sys -import tempfile - -import pytest +import weakref import pyansys_logging @@ -85,11 +83,11 @@ def test_default_file_handlers(): assert "LEVEL - INSTANCE NAME - MODULE - FUNCTION - MESSAGE" in content[4] assert "INFO - - test_pyansys_logging - test_default_file_handlers - Test PyProject.Log" in content[5] - # Remove file's handlers and delete the file. - for handler in test_logger.logger.handlers: - if isinstance(handler, logging.FileHandler): - handler.close() - test_logger.logger.removeHandler(handler) + + # Delete the logger and its file handler. + test_logger_ref = weakref.ref(test_logger) + del test_logger + assert test_logger_ref() is None os.remove(file_logger) @@ -114,11 +112,10 @@ def test_file_handlers(): assert "LEVEL - INSTANCE NAME - MODULE - FUNCTION - MESSAGE" in content[4] assert "INFO - - test_pyansys_logging - test_file_handlers - Test Misc File" in content[5] - # Remove file's handlers and delete the file. - for handler in test_logger.logger.handlers: - if isinstance(handler, logging.FileHandler): - handler.close() - test_logger.logger.removeHandler(handler) + # Delete the logger and its file handler. + test_logger_ref = weakref.ref(test_logger) + del test_logger + assert test_logger_ref() is None os.remove(file_logger) From dad78613e929552b9f412e84d91db0ec20f9c403 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Tue, 4 Jan 2022 10:21:44 +0100 Subject: [PATCH 26/32] Improve logging.Handler section. --- doc/source/guidelines/logging.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 2479e4b49..8d347d693 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -54,7 +54,7 @@ This argument specifies how to handle the stream. Enable/Disable handlers ~~~~~~~~~~~~~~~~~~~~~~~ Sometimes the user might want to disable specific handlers such as a -file handler where log messages are written. If so, the existing +file handler where log messages are written. If so, the existing handler must be properly closed and removed. Otherwise the file access might be denied later when you try to write new log content. @@ -132,7 +132,7 @@ module for a PyAnsys library, where a PyAnsys library is used to extend or expose features from an Ansys application, product, or service that may be local or remote. -This section describes two two main loggers for a PyAnsys library that +This section describes two main loggers for a PyAnsys library that exposes or extends a service based application, the *Global logger* and the *Instance logger*. These loggers are customized classes that wrap :class:`logging.Logger` from :mod:`logging` module and add specific @@ -298,7 +298,8 @@ AEDT product has its own internal logger called the message manager made of 3 ma * *Project*: related to the project * *Design*: related to the design (most specific destination of each 3 loggers.) -The message manager is not using the standard python logging module and this might be a problem later when exporting messages and data from each ANSYS product to a common tool. In most of the cases, it is easier to work with the standard python module to extract data. +The message manager is not using the standard python logging module and this might be a problem later when exporting messages and data from each ANSYS product to a common tool. +In most of the cases, it is easier to work with the standard python module to extract data. In order to overcome this limitation, the existing message manager is wrapped into a logger based on the standard python `logging `__ module. @@ -310,7 +311,7 @@ In order to overcome this limitation, the existing message manager is wrapped in **Figure 1: Loggers message passing flow.** -To do so, we created a class called LogHandler based on logging.Handler. +This wrapper implementation boils down to a custom handler. It is based on a class inherited from logging.Handler. The initializer of this class will require the message manager to be passed as an argument in order to link the standard logging service with the ANSYS internal message manager. .. code:: python @@ -319,7 +320,7 @@ The initializer of this class will require the message manager to be passed as a def __init__(self, internal_app_messenger, log_destination, level=logging.INFO): logging.Handler.__init__(self, level) - # destination is used if your internal message manager + # destination is used if when the internal message manager # is made of several different logs. Otherwise it is not relevant. self.destination = log_destination self.messenger = internal_app_messenger From a49eccfd1028aeac2158870bc3a786bd2a2afa59 Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Tue, 4 Jan 2022 10:45:44 +0100 Subject: [PATCH 27/32] Use the tmpdir fixture. --- logging/test_pyansys_logging.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/logging/test_pyansys_logging.py b/logging/test_pyansys_logging.py index 269d23222..629b2c7aa 100644 --- a/logging/test_pyansys_logging.py +++ b/logging/test_pyansys_logging.py @@ -91,14 +91,13 @@ def test_default_file_handlers(): os.remove(file_logger) -def test_file_handlers(): +def test_file_handlers(tmpdir): """Activate a file handler different from `PyProject.log`.""" content = None - current_dirctory = os.getcwd() - file_logger = os.path.join(current_dirctory, "test_logger.txt") - if os.path.exists(file_logger): - os.remove(file_logger) + + file_logger = tmpdir.mkdir("sub").join("test_logger.txt") + test_logger = pyansys_logging.Logger(to_file=True, filename=file_logger) test_logger.info("Test Misc File") @@ -116,7 +115,6 @@ def test_file_handlers(): test_logger_ref = weakref.ref(test_logger) del test_logger assert test_logger_ref() is None - os.remove(file_logger) class CaptureStdOut: From ee2c1b3fe79fafb0d0a5a8df4b714810bcb2ac4c Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Tue, 4 Jan 2022 19:53:35 +0100 Subject: [PATCH 28/32] Improve the wrapping section. --- doc/source/guidelines/logging.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/guidelines/logging.rst b/doc/source/guidelines/logging.rst index 8d347d693..8154082e3 100644 --- a/doc/source/guidelines/logging.rst +++ b/doc/source/guidelines/logging.rst @@ -275,16 +275,16 @@ You can use this logger like this: Wrapping Other Loggers ~~~~~~~~~~~~~~~~~~~~~~ A product, due to its architecture can be made of several loggers. -The ``logging`` library features allows to work a finite number of loggers. +The ``logging`` library features allows to work with a finite number of loggers. The factory function logging.getLogger() helps to access each logger by its name. In addition of this naming-mappings, a hierachy can be established to structure the loggers -order. +parenting and their connection. -For instance, if an ANSYS product is using a custom logger encapsulated inside the product itself, you might benefit from exposing it through the standard python tools. -It is recommended to use the standard library as much as possible. It will benefit every contributor to your project by exposing common tools that are widely spread. +For instance, if an ANSYS product is using a pre-exsiting custom logger encapsulated inside the product itself, the will benefit from exposing it through the standard python tools. +It is recommended to use the standard library as much as possible. It will facilitate every contribution -both external and internal- to the by exposing common tools that are widely spread. Each developer will be able to operate quickly and autonomously. -Your project will take advantage of the entire set of features exposed in the standard logger and all the upcoming improvements. +The project will take advantage of the entire set of features exposed in the standard logger and all the upcoming improvements. Create a custom log handler to catch each product message and redirect them on another logger: ============================================================================================== From ef7b052baea6d3ffa7af4bb5016bd2d17a08b865 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:55:19 +0100 Subject: [PATCH 29/32] Update logging/test_pyansys_logging.py Co-authored-by: Alex Kaszynski --- logging/test_pyansys_logging.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/logging/test_pyansys_logging.py b/logging/test_pyansys_logging.py index 629b2c7aa..f2f69e2d3 100644 --- a/logging/test_pyansys_logging.py +++ b/logging/test_pyansys_logging.py @@ -62,35 +62,6 @@ def test_level_stdout(): assert os.path.exists(os.path.exists(os.path.join(os.getcwd(), "PyProject.log"))) -def test_default_file_handlers(): - """Activate the `PyProject.log` file handler.""" - - current_dirctory = os.getcwd() - file_logger = os.path.join(current_dirctory, "PyProject.log") - if os.path.exists(file_logger): - os.remove(file_logger) - - content = None - test_logger = pyansys_logging.Logger(to_file=True) - test_logger.info("Test PyProject.Log") - - with open(file_logger, "r") as f: - content = f.readlines() - - assert len(content) == 6 - assert "NEW SESSION" in content[2] - assert "===============================================================================" in content[3] - assert "LEVEL - INSTANCE NAME - MODULE - FUNCTION - MESSAGE" in content[4] - assert "INFO - - test_pyansys_logging - test_default_file_handlers - Test PyProject.Log" in content[5] - - - # Delete the logger and its file handler. - test_logger_ref = weakref.ref(test_logger) - del test_logger - assert test_logger_ref() is None - os.remove(file_logger) - - def test_file_handlers(tmpdir): """Activate a file handler different from `PyProject.log`.""" From 7de2923f452d8f6d5339d419240d08deeb3b7550 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:55:34 +0100 Subject: [PATCH 30/32] Update logging/pyansys_logging.py Co-authored-by: Alex Kaszynski --- logging/pyansys_logging.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/logging/pyansys_logging.py b/logging/pyansys_logging.py index 383442e29..2d4479304 100644 --- a/logging/pyansys_logging.py +++ b/logging/pyansys_logging.py @@ -187,21 +187,7 @@ class Logger: def __init__( self, level=logging.DEBUG, to_file=False, to_stdout=True, filename=FILE_NAME ): - """Customized logger class for PyProject. - - Parameters - ---------- - level : str, optional - Level of logging as defined in the package ``logging``. By - default ``'DEBUG'``. - to_file : bool, optional - To record the logs in a file, by default ``False``. - to_stdout : bool, optional - To output the logs to the standard output, which is the - command line. By default ``True``. - filename : str, optional - Name of the output file. By default ``"PyProject.log"``. - """ + """Initialize Logger class.""" self.logger = logging.getLogger( "pyproject_global" From 37ddf87add166a65a270124ad4264b76afdbbbd3 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:55:55 +0100 Subject: [PATCH 31/32] Update logging/test_pyansys_logging.py Co-authored-by: Alex Kaszynski --- logging/test_pyansys_logging.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/logging/test_pyansys_logging.py b/logging/test_pyansys_logging.py index f2f69e2d3..44eb377ad 100644 --- a/logging/test_pyansys_logging.py +++ b/logging/test_pyansys_logging.py @@ -65,8 +65,6 @@ def test_level_stdout(): def test_file_handlers(tmpdir): """Activate a file handler different from `PyProject.log`.""" - content = None - file_logger = tmpdir.mkdir("sub").join("test_logger.txt") test_logger = pyansys_logging.Logger(to_file=True, filename=file_logger) From 6a19e7c97f880b3675e300396e582ee7fba50ebe Mon Sep 17 00:00:00 2001 From: Maxime Rey Date: Tue, 4 Jan 2022 19:59:24 +0100 Subject: [PATCH 32/32] Add the cleanup argument. --- logging/pyansys_logging.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/logging/pyansys_logging.py b/logging/pyansys_logging.py index 2d4479304..ff7288726 100644 --- a/logging/pyansys_logging.py +++ b/logging/pyansys_logging.py @@ -185,7 +185,7 @@ class Logger: _instances = {} def __init__( - self, level=logging.DEBUG, to_file=False, to_stdout=True, filename=FILE_NAME + self, level=logging.DEBUG, to_file=False, to_stdout=True, filename=FILE_NAME, cleanup=True ): """Initialize Logger class.""" @@ -216,7 +216,7 @@ def __init__( self.logger ) # Using logger to record unhandled exceptions. - self._cleanup = True + self.cleanup = cleanup def log_to_file(self, filename=FILE_NAME, level=LOG_LEVEL): """Add file handler to logger. @@ -395,7 +395,7 @@ def handle_exception(exc_type, exc_value, exc_traceback): def __del__(self): """Close the logger and all its handlers.""" self.logger.debug("Collecting logger") - if self._cleanup: + if self.cleanup: try: for handler in self.logger.handlers: handler.close() @@ -407,7 +407,7 @@ def __del__(self): except Exception: pass else: - self.logger.debug("Collecting but not exiting due to 'self._cleanup = False'") + self.logger.debug("Collecting but not exiting due to 'cleanup = False'") def add_file_handler(logger, filename=FILE_NAME, level=LOG_LEVEL, write_headers=False):