ae-core 0.3.76__tar.gz → 0.3.78__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.58 -->
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.59 -->
2
2
  ### GNU GENERAL PUBLIC LICENSE
3
3
 
4
4
  Version 3, 29 June 2007
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ae_core
3
- Version: 0.3.76
3
+ Version: 0.3.78
4
4
  Summary: ae namespace module portion core: application core constants, helper functions and base classes
5
5
  Home-page: https://gitlab.com/ae-group/ae_core
6
6
  Author: AndiEcker
@@ -67,15 +67,15 @@ Dynamic: requires-dist
67
67
  Dynamic: requires-python
68
68
  Dynamic: summary
69
69
 
70
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.100 -->
71
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.21 -->
72
- # core 0.3.76
70
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.101 -->
71
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.22 -->
72
+ # core 0.3.78
73
73
 
74
74
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_core/develop?logo=python)](
75
75
  https://gitlab.com/ae-group/ae_core)
76
76
  [![LatestPyPIrelease](
77
- https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.76?logo=python)](
78
- https://gitlab.com/ae-group/ae_core/-/tree/release0.3.76)
77
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.78?logo=python)](
78
+ https://gitlab.com/ae-group/ae_core/-/tree/release0.3.78)
79
79
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_core)](
80
80
  https://pypi.org/project/ae-core/#history)
81
81
 
@@ -1,12 +1,12 @@
1
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.100 -->
2
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.21 -->
3
- # core 0.3.76
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.101 -->
2
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.22 -->
3
+ # core 0.3.78
4
4
 
5
5
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_core/develop?logo=python)](
6
6
  https://gitlab.com/ae-group/ae_core)
7
7
  [![LatestPyPIrelease](
8
- https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.76?logo=python)](
9
- https://gitlab.com/ae-group/ae_core/-/tree/release0.3.76)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.78?logo=python)](
9
+ https://gitlab.com/ae-group/ae_core/-/tree/release0.3.78)
10
10
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_core)](
11
11
  https://pypi.org/project/ae-core/#history)
12
12
 
@@ -218,6 +218,7 @@ application code.
218
218
  .. _debug-level-constants:
219
219
 
220
220
  """
221
+ # pylint: disable=too-many-lines
221
222
  import datetime
222
223
  import faulthandler
223
224
  import logging
@@ -230,7 +231,7 @@ import traceback
230
231
  import weakref
231
232
 
232
233
  from io import StringIO
233
- from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple, Union, cast
234
+ from typing import Any, Callable, Optional, TextIO, Union, cast
234
235
 
235
236
  from ae.base import ( # type: ignore
236
237
  BUILD_CONFIG_FILE, DATE_TIME_ISO, DEF_ENCODE_ERRORS, PY_EXT, PY_INIT, PY_MAIN,
@@ -242,7 +243,7 @@ from ae.paths import (
242
243
  from ae.updater import check_all # type: ignore
243
244
 
244
245
 
245
- __version__ = '0.3.76'
246
+ __version__ = '0.3.78'
246
247
 
247
248
 
248
249
  # package and permissions handling defaults for all platforms and frameworks
@@ -268,7 +269,7 @@ if os_platform == 'android': # pragma: no cover
268
269
  from android.permissions import request_permissions, Permission # type: ignore # pylint: disable=import-error
269
270
  from jnius import autoclass # type: ignore
270
271
 
271
- def request_app_permissions(callback: Optional[Callable[[List[Permission], List[bool]], None]] = None):
272
+ def request_app_permissions(callback: Optional[Callable[[list[Permission], list[bool]], None]] = None):
272
273
  """ request app/service permissions on Android OS.
273
274
 
274
275
  :param callback: optional callback receiving two list arguments with identical length,
@@ -321,11 +322,13 @@ DEBUG_LEVEL_DISABLED: int = 0 #: lowest debug level - only display logging
321
322
  DEBUG_LEVEL_ENABLED: int = 1 #: minimum debugging info - display logging levels WARNING or higher.
322
323
  DEBUG_LEVEL_VERBOSE: int = 2 #: verbose debug info - display logging levels INFO/DEBUG or higher.
323
324
 
324
- DEBUG_LEVELS: Dict[int, str] = {DEBUG_LEVEL_DISABLED: 'disabled', DEBUG_LEVEL_ENABLED: 'enabled',
325
+ DEBUG_LEVELS: dict[int, str] = {DEBUG_LEVEL_DISABLED: 'disabled',
326
+ DEBUG_LEVEL_ENABLED: 'enabled',
325
327
  DEBUG_LEVEL_VERBOSE: 'verbose'}
326
328
  """ numeric ids and names of all supported debug levels. """
327
329
 
328
- LOGGING_LEVELS: Dict[int, int] = {DEBUG_LEVEL_DISABLED: logging.WARNING, DEBUG_LEVEL_ENABLED: logging.INFO,
330
+ LOGGING_LEVELS: dict[int, int] = {DEBUG_LEVEL_DISABLED: logging.WARNING,
331
+ DEBUG_LEVEL_ENABLED: logging.INFO,
329
332
  DEBUG_LEVEL_VERBOSE: logging.DEBUG}
330
333
  """ association between ae debug levels and python logging levels. """
331
334
 
@@ -367,6 +370,13 @@ def logger_late_init():
367
370
  _LOGGER = logging.getLogger(__name__) # reset (_LOGGER = None) done in _unregister_app_instance()
368
371
 
369
372
 
373
+ def logger_shutdown():
374
+ """ reset logger and logging module. """
375
+ global _LOGGER # pylint: disable=global-statement
376
+ _LOGGER = None
377
+ logging.shutdown()
378
+
379
+
370
380
  _MULTI_THREADING_ACTIVATED: bool = False #: flag if threading is used in your application
371
381
 
372
382
 
@@ -382,6 +392,7 @@ def _deactivate_multi_threading():
382
392
  _MULTI_THREADING_ACTIVATED = False
383
393
 
384
394
 
395
+ # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements
385
396
  def print_out(*objects, sep: str = " ", end: str = "\n", file: Optional[TextIO] = None, flush: bool = False,
386
397
  encode_errors_def: str = DEF_ENCODE_ERRORS, logger: Optional['logging.Logger'] = None,
387
398
  app: Optional['AppBase'] = None, **kwargs):
@@ -427,6 +438,7 @@ def print_out(*objects, sep: str = " ", end: str = "\n", file: Optional[TextIO]
427
438
  else:
428
439
  app = main_app
429
440
 
441
+ # pylint: disable=too-many-boolean-expressions
430
442
  if processing:
431
443
  file = ori_std_out
432
444
  elif logger is not None and file is None and (
@@ -477,6 +489,42 @@ def print_out(*objects, sep: str = " ", end: str = "\n", file: Optional[TextIO]
477
489
  break
478
490
 
479
491
 
492
+ def debug_out(*objects, **kwargs):
493
+ """ print out if debug mode is enabled. if app instance is available, then use :meth:`AppBase.debug_out` instead.
494
+
495
+ :param objects: see argument description of :meth:`AppBase.debug_out`.
496
+ :param kwargs: see argument description of :meth:`AppBase.debug_out`.
497
+ """
498
+ getattr(main_app_instance(), 'debug_out', print_out)(*objects, **kwargs)
499
+
500
+
501
+ def verbose_out(*objects, **kwargs):
502
+ """ print out if verbose debug mode is enabled. if app instance is available, then use :meth:`AppBase.verbose_out`.
503
+
504
+ :param objects: see argument description of :meth:`AppBase.verbose_out`.
505
+ :param kwargs: see argument description of :meth:`AppBase.verbose_out`.
506
+ """
507
+ getattr(main_app_instance(), 'verbose_out', print_out)(*objects, **kwargs)
508
+
509
+
510
+ def is_debug() -> bool:
511
+ """ determine if the debug level of the main app instance is set/enabled.
512
+
513
+ :return: True if the debugging of the main app instance is enabled or if the app main instance
514
+ did not get registered, else False (main app instance exists, but no debugging enabled).
515
+ """
516
+ return getattr(main_app_instance(), 'debug', True)
517
+
518
+
519
+ def is_verbose() -> bool:
520
+ """ determine if the verbose debug level of the main app instance is set/enabled.
521
+
522
+ :return: True if the debugging of the main app instance is enabled or if the app main instance
523
+ did not get registered, else False (main app exists, but no verbose debugging enabled).
524
+ """
525
+ return getattr(main_app_instance(), 'verbose', True)
526
+
527
+
480
528
  APP_KEY_SEP: str = '@' #: separator character used in :attr:`~AppBase.app_key` of :class:`AppBase` instance
481
529
 
482
530
  # had to use type comment because the following line is throwing an error in the Sphinx docs make:
@@ -505,7 +553,7 @@ def main_app_instance() -> Optional['AppBase']:
505
553
  return _APP_INSTANCES.get(_MAIN_APP_INST_KEY)
506
554
 
507
555
 
508
- def registered_app_names() -> List[str]:
556
+ def registered_app_names() -> list[str]:
509
557
  """ determine the app names of all registered/running applications. """
510
558
  with app_inst_lock:
511
559
  return [app.app_name for app in _APP_INSTANCES.values()]
@@ -541,8 +589,7 @@ def _unregister_app_instance(app_key: str) -> Optional['AppBase']:
541
589
  :return: removed :class:`AppBase` instance.
542
590
  """
543
591
  with app_inst_lock:
544
- global _LOGGER # pylint: disable=global-statement
545
- _LOGGER = None
592
+ logger_shutdown()
546
593
 
547
594
  global _MAIN_APP_INST_KEY # pylint: disable=global-statement
548
595
  app = _APP_INSTANCES.pop(app_key, None)
@@ -563,7 +610,7 @@ def _shut_down_sub_app_instances(timeout: Optional[float] = None):
563
610
  acquisition of the threading locks of :data:`the ae log file <log_file_lock>` and the
564
611
  :data:`app instances <app_inst_lock>`.
565
612
  """
566
- aqc_kwargs: Dict[str, Any] = ({'blocking': False} if timeout is None else {'timeout': timeout})
613
+ aqc_kwargs: dict[str, Any] = ({'blocking': False} if timeout is None else {'timeout': timeout})
567
614
  blocked = app_inst_lock.acquire(**aqc_kwargs) # pylint: disable=consider-using-with
568
615
  for app in reversed(list(_APP_INSTANCES.values())): # list() because the weak ref dict gets changed in the loop
569
616
  if not app.is_main:
@@ -591,7 +638,7 @@ class _PrintingReplicator:
591
638
  :param any_str: string or bytes to output.
592
639
  """
593
640
  message = any_str.decode() if isinstance(any_str, bytes) else any_str
594
- app_streams: List[Tuple[Optional[AppBase], TextIO]] = []
641
+ app_streams: list[tuple[Optional[AppBase], TextIO]] = []
595
642
  with log_file_lock, app_inst_lock:
596
643
  for app in list(_APP_INSTANCES.values()):
597
644
  stream = app.log_file_check(app.active_log_stream) # check if log rotation or buf-to-file-switch needed
@@ -649,7 +696,7 @@ def _join_app_threads(timeout: Optional[float] = None):
649
696
  _deactivate_multi_threading()
650
697
 
651
698
 
652
- class AppBase:
699
+ class AppBase: # pylint: disable=too-many-instance-attributes
653
700
  """ provides easy logging and debugging for your application.
654
701
 
655
702
  most applications only need a single instance of this class; apps with threads could create separate instances
@@ -693,9 +740,10 @@ class AppBase:
693
740
  _log_file_name: str = "" #: log file name
694
741
  _log_with_timestamp: Union[bool, str] = False #: True of strftime format string to enable timestamp
695
742
  _nul_std_out: Optional[TextIO] = None #: logging null stream
696
- py_log_params: Dict[str, Any] = {} #: dict of config parameters for py logging
743
+ py_log_params: dict[str, Any] = {} #: dict of config parameters for py logging
697
744
  _shut_down: bool = False #: True if this app instance got shut down already
698
745
 
746
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
699
747
  def __init__(self, app_title: str = '', app_name: str = '', app_version: str = '', sys_env_id: str = '',
700
748
  debug_level: int = DEBUG_LEVEL_VERBOSE, multi_threading: bool = False, suppress_stdout: bool = False):
701
749
  """ initialize a new :class:`AppBase` instance.
@@ -749,7 +797,7 @@ class AppBase:
749
797
  else: # pragma: no cover
750
798
  self.vpo(f"AppBase.__init__() upgrade check skipped because {app_path=} != {cwd_path=}")
751
799
 
752
- def _init_path_placeholders(self):
800
+ def _init_path_placeholders(self): # pylint: disable=too-many-locals
753
801
  """ correct app_name/main_app_name, the related path placeholders and ensure write access for some of them. """
754
802
  # correct app name guess, init by :mod:`ae.paths` (main app from ("", 'pyTstConsAppKey', '_jb_pytest_runner'))
755
803
  PATH_PLACEHOLDERS['main_app_name'] = PATH_PLACEHOLDERS['app_name'] = app_name = self.app_name
@@ -881,7 +929,8 @@ class AppBase:
881
929
 
882
930
  return None
883
931
 
884
- def init_logging(self, py_logging_params: Optional[Dict[str, Any]] = None, log_file_name: str = "",
932
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
933
+ def init_logging(self, py_logging_params: Optional[dict[str, Any]] = None, log_file_name: str = "",
885
934
  log_file_size_max: float = LOG_FILE_MAX_SIZE, log_with_timestamp: Union[bool, str] = False,
886
935
  disable_buffering: bool = False):
887
936
  """ initialize the logging system.
@@ -1000,7 +1049,7 @@ class AppBase:
1000
1049
  :param kwargs: all the other supported kwargs of this method are documented
1001
1050
  :func:`at the print_out() function of this module <print_out>`.
1002
1051
 
1003
- this method has an alias named :meth:`.po`
1052
+ .. hint:: this method has an alias named :meth:`.po`
1004
1053
  """
1005
1054
  if file is None and main_app_instance() is not self: # self.is_main==True when main_app_instance() is None
1006
1055
  with log_file_lock:
@@ -1013,23 +1062,16 @@ class AppBase:
1013
1062
 
1014
1063
  po = print_out #: alias of method :meth:`.print_out`
1015
1064
 
1016
- def debug_out(self, *objects, minimum_debug_level: int = DEBUG_LEVEL_ENABLED, **kwargs):
1017
- """ special debug version of :func:`builtin print() function <print>`.
1018
-
1019
- this method will print out the passed objects only if the :attr:`current debug level
1020
- <.core.AppBase.debug_level>` of this app instance is higher than the value passed into the
1021
- :paramref:`~debug_out.minimum_debug_level` argument. in this case the print-out will be
1022
- delegated onto the :meth:`~.print_out`.
1065
+ def debug_out(self, *objects, **kwargs):
1066
+ """ print objects if :attr:`the current debug level <.core.AppBase.debug_level>`of this app instance is enabled.
1023
1067
 
1024
1068
  :param objects: objects to be printed out.
1025
- :param minimum_debug_level: minimum debug level to print the passed objects.
1026
1069
  :param kwargs: all the supported kwargs of this method are documented at the
1027
- :func:`print_out() function <core.print_out>` of the :mod:`~ae.core` module
1028
- (including the :paramref:`~.print_out.file` argument).
1070
+ :func:`print_out() function <core.print_out>`.
1029
1071
 
1030
- this method has an alias named :meth:`.dpo`.
1072
+ .. hint:: this method has an alias named :meth:`.dpo`.
1031
1073
  """
1032
- if self.debug_level >= minimum_debug_level:
1074
+ if self.debug_level >= DEBUG_LEVEL_ENABLED:
1033
1075
  self.po(*objects, **kwargs)
1034
1076
 
1035
1077
  dpo = debug_out #: alias of method :meth:`.debug_out`
@@ -1043,7 +1085,7 @@ class AppBase:
1043
1085
  supported kwargs of this method are documented at the
1044
1086
  :func:`print_out() function <~ae.core.print_out>` of the :mod:`~.core` module.
1045
1087
 
1046
- this method has an alias named :meth:`.vpo`.
1088
+ .. hint:: this method has an alias named :meth:`.vpo`.
1047
1089
  """
1048
1090
  if self.debug_level >= DEBUG_LEVEL_VERBOSE:
1049
1091
  self.po(*objects, **kwargs)
@@ -1061,11 +1103,13 @@ class AppBase:
1061
1103
  """
1062
1104
  if self._shut_down:
1063
1105
  return
1064
- aqc_kwargs: Dict[str, Any] = {'blocking': False} if timeout is None else {'timeout': timeout}
1106
+ aqc_kwargs: dict[str, Any] = {'blocking': False} if timeout is None else {'timeout': timeout}
1065
1107
  is_main_app_instance = main_app_instance() is self # self.is_main==True when main_app_instance() is None
1066
1108
  force = is_main_app_instance and exit_code # prevent deadlock on app error exit/shutdown
1067
1109
 
1068
1110
  if exit_code is not None:
1111
+ if not 0 <= exit_code <= 255:
1112
+ self.po(f" ### extended exit code {exit_code}! most shells only get 8 bits(0..255)=={exit_code % 256}")
1069
1113
  self.po(f"##### {'forced ' if force else ''}shutdown of {self.app_name} with {exit_code=}", logger=_LOGGER)
1070
1114
 
1071
1115
  # pylint: disable-next=consider-using-with
@@ -1119,7 +1163,9 @@ class AppBase:
1119
1163
  else:
1120
1164
  # pylint: disable-next=unspecified-encoding, consider-using-with
1121
1165
  std_out = self._nul_std_out = open(os.devnull, 'w')
1166
+ # noinspection PyInvalidCast
1122
1167
  sys.stdout = cast(TextIO, _PrintingReplicator(sys_out_obj=std_out))
1168
+ # noinspection PyInvalidCast
1123
1169
  sys.stderr = cast(TextIO, _PrintingReplicator(sys_out_obj=ori_std_err))
1124
1170
  else:
1125
1171
  if is_main_app_instance:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ae_core
3
- Version: 0.3.76
3
+ Version: 0.3.78
4
4
  Summary: ae namespace module portion core: application core constants, helper functions and base classes
5
5
  Home-page: https://gitlab.com/ae-group/ae_core
6
6
  Author: AndiEcker
@@ -67,15 +67,15 @@ Dynamic: requires-dist
67
67
  Dynamic: requires-python
68
68
  Dynamic: summary
69
69
 
70
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.100 -->
71
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.21 -->
72
- # core 0.3.76
70
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.101 -->
71
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.22 -->
72
+ # core 0.3.78
73
73
 
74
74
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_core/develop?logo=python)](
75
75
  https://gitlab.com/ae-group/ae_core)
76
76
  [![LatestPyPIrelease](
77
- https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.76?logo=python)](
78
- https://gitlab.com/ae-group/ae_core/-/tree/release0.3.76)
77
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.78?logo=python)](
78
+ https://gitlab.com/ae-group/ae_core/-/tree/release0.3.78)
79
79
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_core)](
80
80
  https://pypi.org/project/ae-core/#history)
81
81
 
@@ -1,4 +1,4 @@
1
- # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.58
1
+ # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.59
2
2
  [build-system]
3
3
  requires = ["setuptools>=42", "wheel"]
4
4
  build-backend = "setuptools.build_meta"
@@ -1,4 +1,4 @@
1
- # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.58
1
+ # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.59
2
2
  """ setup of ae namespace module portion core: application core constants, helper functions and base classes. """
3
3
  # noinspection PyUnresolvedReferences
4
4
  import sys
@@ -23,15 +23,15 @@ setup_kwargs = {
23
23
  'install_requires': ['ae_base', 'ae_paths', 'ae_updater'],
24
24
  'keywords': ['configuration', 'development', 'environment', 'productivity'],
25
25
  'license': 'GPL-3.0-or-later',
26
- 'long_description': ('<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.100 -->\n'
27
- '<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.21 -->\n'
28
- '# core 0.3.76\n'
26
+ 'long_description': ('<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.101 -->\n'
27
+ '<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.22 -->\n'
28
+ '# core 0.3.78\n'
29
29
  '\n'
30
30
  '[![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_core/develop?logo=python)](\n'
31
31
  ' https://gitlab.com/ae-group/ae_core)\n'
32
32
  '[![LatestPyPIrelease](\n'
33
- ' https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.76?logo=python)](\n'
34
- ' https://gitlab.com/ae-group/ae_core/-/tree/release0.3.76)\n'
33
+ ' https://img.shields.io/gitlab/pipeline/ae-group/ae_core/release0.3.78?logo=python)](\n'
34
+ ' https://gitlab.com/ae-group/ae_core/-/tree/release0.3.78)\n'
35
35
  '[![PyPIVersions](https://img.shields.io/pypi/v/ae_core)](\n'
36
36
  ' https://pypi.org/project/ae-core/#history)\n'
37
37
  '\n'
@@ -108,7 +108,7 @@ setup_kwargs = {
108
108
  'Source': 'https://ae.readthedocs.io/en/latest/_modules/ae/core.html'},
109
109
  'python_requires': '>=3.9',
110
110
  'url': 'https://gitlab.com/ae-group/ae_core',
111
- 'version': '0.3.76',
111
+ 'version': '0.3.78',
112
112
  'zip_safe': True,
113
113
  }
114
114
 
@@ -12,23 +12,53 @@ from typing import cast, Any, TextIO
12
12
  from unittest.mock import patch
13
13
 
14
14
 
15
- from ae.base import DATE_TIME_ISO, force_encoding, norm_path, read_file, write_file
15
+ import ae.paths # for patch tests of ae.paths.PATH_PLACEHOLDERS
16
+ import ae.core # for patch tests of ae.core.PATH_PLACEHOLDERS
17
+
18
+ from ae.base import (
19
+ DATE_TIME_ISO, force_encoding, norm_path, os_path_dirname, os_path_isfile, os_path_splitext, read_file, write_file)
16
20
  from ae.paths import PATH_PLACEHOLDERS, placeholder_path, coll_folders, Collector
17
21
  # noinspection PyProtectedMember
18
22
  from ae.core import (
19
23
  APP_KEY_SEP, DEBUG_LEVELS, DEBUG_LEVEL_DISABLED, DEBUG_LEVEL_ENABLED, DEBUG_LEVEL_VERBOSE,
20
24
  LOG_FILE_IDX_WIDTH, MAX_NUM_LOG_FILES,
21
25
  activate_multi_threading, _deactivate_multi_threading, hide_dup_line_prefix, main_app_instance, print_out,
26
+ debug_out, verbose_out, is_debug, is_verbose,
22
27
  registered_app_names,
23
28
  AppBase, _PrintingReplicator)
24
- import ae.paths # for patch tests of ae.paths.PATH_PLACEHOLDERS
25
- import ae.core # for patch tests of ae.core.PATH_PLACEHOLDERS
26
29
 
27
30
 
28
31
  __version__ = '3.6.9dev-test' # used for automatic app version find tests
29
32
 
30
33
 
31
34
  class TestCoreHelpers:
35
+ def test_debug_out(self, capsys, restore_app_env):
36
+ app = AppBase()
37
+ app.debug_level = DEBUG_LEVEL_VERBOSE
38
+
39
+ debug_out('tst console output')
40
+
41
+ out, err = capsys.readouterr()
42
+ assert 'tst console output' in out
43
+ assert err == ""
44
+
45
+ app.debug_level = DEBUG_LEVEL_ENABLED
46
+
47
+ debug_out('tst console output')
48
+
49
+ out, err = capsys.readouterr()
50
+ assert 'tst console output' in out
51
+ assert err == ""
52
+
53
+ app.debug_level = DEBUG_LEVEL_DISABLED
54
+
55
+ debug_out('tst console output')
56
+
57
+ out, err = capsys.readouterr()
58
+ assert 'tst console output' not in out
59
+ assert out == ""
60
+ assert err == ""
61
+
32
62
  def test_hide_dup_line_prefix(self):
33
63
  l1 = "<t_s_t>"
34
64
  l2 = l1
@@ -39,6 +69,34 @@ class TestCoreHelpers:
39
69
  l2 = l1[:3] + l1
40
70
  assert hide_dup_line_prefix(l1, l2) == " " * 3 + l1
41
71
 
72
+ def test_is_debug(self, restore_app_env):
73
+ app = AppBase()
74
+ app.debug_level = DEBUG_LEVEL_DISABLED
75
+
76
+ assert not is_debug()
77
+
78
+ app.debug_level = DEBUG_LEVEL_ENABLED
79
+
80
+ assert is_debug()
81
+
82
+ app.debug_level = DEBUG_LEVEL_VERBOSE
83
+
84
+ assert is_debug()
85
+
86
+ def test_is_verbose(self, restore_app_env):
87
+ app = AppBase()
88
+ app.debug_level = DEBUG_LEVEL_DISABLED
89
+
90
+ assert not is_verbose()
91
+
92
+ app.debug_level = DEBUG_LEVEL_ENABLED
93
+
94
+ assert not is_verbose()
95
+
96
+ app.debug_level = DEBUG_LEVEL_VERBOSE
97
+
98
+ assert is_verbose()
99
+
42
100
  def test_print_out_basics(self, capsys):
43
101
  print_out()
44
102
  out, err = capsys.readouterr()
@@ -85,6 +143,34 @@ class TestCoreHelpers:
85
143
  assert len(registered_app_names()) == 1
86
144
  assert app.app_name == registered_app_names()[0]
87
145
 
146
+ def test_verbose_out(self, capsys, restore_app_env):
147
+ app = AppBase()
148
+ app.debug_level = DEBUG_LEVEL_VERBOSE
149
+
150
+ verbose_out('tst console output')
151
+
152
+ out, err = capsys.readouterr()
153
+ assert 'tst console output' in out
154
+ assert err == ""
155
+
156
+ app.debug_level = DEBUG_LEVEL_ENABLED
157
+
158
+ verbose_out('tst console output')
159
+
160
+ out, err = capsys.readouterr()
161
+ assert 'tst console output' not in out
162
+ assert out == ""
163
+ assert err == ""
164
+
165
+ app.debug_level = DEBUG_LEVEL_DISABLED
166
+
167
+ verbose_out('tst console output')
168
+
169
+ out, err = capsys.readouterr()
170
+ assert 'tst console output' not in out
171
+ assert out == ""
172
+ assert err == ""
173
+
88
174
 
89
175
  class TestPrintingReplicator:
90
176
  def test_init(self):
@@ -149,7 +235,7 @@ class TestAeLogging:
149
235
  for idx in range(MAX_NUM_LOG_FILES + 9):
150
236
  for line_no in range(16): # full loop is creating 1 kB of log entries (16 * 64 bytes)
151
237
  app.po("TestBaseLogEntry{: >26}{: >26}".format(idx, line_no))
152
- assert os.path.exists(log_file)
238
+ assert os_path_isfile(log_file)
153
239
  finally:
154
240
  assert delete_files(log_file, keep_ext=True) == MAX_NUM_LOG_FILES + 1
155
241
 
@@ -157,7 +243,7 @@ class TestAeLogging:
157
243
  log_file = 'test_ae_cov_log.log'
158
244
  valid_log_content = "TestBaseLogEntry"
159
245
  invalid_log_content = "NeverAppearInLogFile"
160
- fb, ext = os.path.splitext(log_file) # simulate a left-over log file from the last app run - coverage
246
+ fb, ext = os_path_splitext(log_file) # simulate a left-over log file from the last app run - coverage
161
247
  idx = 1
162
248
  write_file(f"{fb}-{idx:0>{LOG_FILE_IDX_WIDTH}}{ext}",
163
249
  f"log file content to test left-over from last app run{invalid_log_content}")
@@ -167,7 +253,7 @@ class TestAeLogging:
167
253
  for idx in range(MAX_NUM_LOG_FILES + 9):
168
254
  for line_no in range(16): # full loop is creating 1 kB of log entries (16 * 64 bytes)
169
255
  app.po(f"{valid_log_content}{idx: >26}{line_no: >26}")
170
- assert os.path.exists(log_file)
256
+ assert os_path_isfile(log_file)
171
257
  finally:
172
258
  contents = delete_files(log_file, keep_ext=True, ret_type='contents')
173
259
  assert len(contents) == MAX_NUM_LOG_FILES + 1
@@ -187,7 +273,7 @@ class TestAeLogging:
187
273
  for idx in range(MAX_NUM_LOG_FILES + 9):
188
274
  for line_no in range(16):
189
275
  app.po("TestBaseLogEntry{: >26}{: >26}".format(idx, line_no))
190
- assert os.path.exists(log_file)
276
+ assert os_path_isfile(log_file)
191
277
  finally:
192
278
  assert delete_files(log_file, keep_ext=True) == MAX_NUM_LOG_FILES + 1
193
279
 
@@ -201,7 +287,7 @@ class TestAeLogging:
201
287
  for idx in range(MAX_NUM_LOG_FILES + 9):
202
288
  for line_no in range(16):
203
289
  app.po("TestBaseLogEntry{: >26}{: >26}".format(idx, line_no))
204
- assert os.path.exists(log_file)
290
+ assert os_path_isfile(log_file)
205
291
  finally:
206
292
  assert delete_files(log_file, keep_ext=True) == MAX_NUM_LOG_FILES + 1
207
293
 
@@ -216,7 +302,7 @@ class TestAeLogging:
216
302
  out, err = capsys.readouterr()
217
303
  assert out == "" and err == ""
218
304
  app.init_logging() # close the log file
219
- assert os.path.exists(log_file)
305
+ assert os_path_isfile(log_file)
220
306
  out, err = capsys.readouterr()
221
307
  assert out == "" and err == ""
222
308
  finally:
@@ -237,7 +323,7 @@ class TestAeLogging:
237
323
  out, err = capsys.readouterr()
238
324
  assert out == "" and err == ""
239
325
  app.init_logging() # close the log file
240
- assert os.path.exists(log_file)
326
+ assert os_path_isfile(log_file)
241
327
  out, err = capsys.readouterr()
242
328
  assert out == "" and err == ""
243
329
  finally:
@@ -251,7 +337,7 @@ class TestAeLogging:
251
337
  app.init_logging(log_file_name=log_file)
252
338
  with pytest.raises(FileNotFoundError):
253
339
  app.log_file_check() # coverage of callee exception
254
- assert not os.path.exists(log_file)
340
+ assert not os_path_isfile(log_file)
255
341
 
256
342
  def test_log_file_flush(self, restore_app_env):
257
343
  log_file = 'test_ae_base_log_flush.log'
@@ -259,7 +345,7 @@ class TestAeLogging:
259
345
  app = AppBase('test_base_log_file_flush')
260
346
  app.init_logging(log_file_name=log_file)
261
347
  app.log_file_check()
262
- assert os.path.exists(log_file)
348
+ assert os_path_isfile(log_file)
263
349
  finally:
264
350
  assert delete_files(log_file) == 1
265
351
 
@@ -281,8 +367,8 @@ class TestAeLogging:
281
367
  # NOT WORKING: capsys.readouterr() returning empty strings
282
368
  # out, err = capsys.readouterr()
283
369
  # assert out.count(tst_out) == 3 and err == ""
284
- assert os.path.exists(mp + log_file)
285
- assert os.path.exists(sp + log_file)
370
+ assert os_path_isfile(mp + log_file)
371
+ assert os_path_isfile(sp + log_file)
286
372
  finally:
287
373
  contents = delete_files(sp + log_file, ret_type='contents')
288
374
  assert len(contents)
@@ -323,8 +409,8 @@ class TestAeLogging:
323
409
  sub.init_logging() # close the sub-app log file created by sub_thread
324
410
  sub_thread.join()
325
411
  app.init_logging() # close the main-app log file
326
- assert os.path.exists(sp + log_file)
327
- assert os.path.exists(mp + log_file)
412
+ assert os_path_isfile(sp + log_file)
413
+ assert os_path_isfile(mp + log_file)
328
414
  finally:
329
415
  contents = delete_files(sp + log_file, ret_type='contents')
330
416
  assert len(contents)
@@ -340,6 +426,7 @@ class TestAeLogging:
340
426
  def test_exception_log_file_flush(self, restore_app_env):
341
427
  app = AppBase('test_exception_base_log_file_flush')
342
428
  # cause/provoke _append_eof_and_flush_file() exceptions for coverage by passing any other non-stream object
429
+ # noinspection PyInvalidCast
343
430
  app._append_eof_and_flush_file(cast(TextIO, None), 'invalid stream')
344
431
 
345
432
  def test_app_instances_reset2(self):
@@ -350,46 +437,40 @@ class TestPythonLogging:
350
437
  """ test python logging module support
351
438
  """
352
439
  def test_log_init(self, restore_app_env):
353
- var_val = dict(version=1,
354
- disable_existing_loggers=False)
440
+ var_val = {'version': 1,
441
+ 'disable_existing_loggers': False}
355
442
  app = AppBase('log_init')
356
443
  app.init_logging(py_logging_params=var_val)
357
444
 
358
445
  assert app.py_log_params == var_val
359
446
 
360
- logging.shutdown()
361
-
362
447
  def test_app_instances_reset1(self):
363
448
  assert main_app_instance() is None
364
449
 
365
450
  def test_logging_params_dict_console_from_init(self, restore_app_env):
366
- var_val = dict(version=1,
367
- disable_existing_loggers=False,
368
- handlers=dict(console={'class': 'logging.StreamHandler',
369
- 'level': logging.INFO}))
451
+ var_val = {'version': 1,
452
+ 'disable_existing_loggers': False,
453
+ 'handlers': {'console': {'class': 'logging.StreamHandler',
454
+ 'level': logging.INFO}}}
370
455
  print(str(var_val))
371
456
 
372
457
  main_app = AppBase('test_python_logging_params_dict_console')
373
458
  main_app.init_logging(py_logging_params=var_val)
374
459
 
375
460
  assert main_app.py_log_params == var_val
376
- logging.shutdown()
377
461
 
378
462
  def test_logging_params_dict_console_from_init_with_log_file(self, restore_app_env):
379
463
  log_file = 'tst_py_log_complex.log'
380
- entry_prefix = "TST LOG ENTRY "
381
-
382
- var_val = dict(version=1,
383
- disable_existing_loggers=False,
384
- handlers=dict(console={'class': 'logging.handlers.RotatingFileHandler',
385
- 'level': logging.INFO,
386
- 'filename': log_file,
387
- 'maxBytes': 33,
388
- 'backupCount': 63}),
389
- loggers={'root': dict(handlers=['console']),
390
- 'ae': dict(handlers=['console']),
391
- 'ae.console': dict(handlers=['console'])}
392
- )
464
+ var_val = {'version': 1,
465
+ 'disable_existing_loggers': False,
466
+ 'handlers': {'console': {'class': 'logging.handlers.RotatingFileHandler',
467
+ 'level': logging.INFO,
468
+ 'filename': log_file,
469
+ 'maxBytes': 33,
470
+ 'backupCount': 63}},
471
+ 'loggers': {'root': {'handlers': ['console']},
472
+ 'ae': {'handlers': ['console']},
473
+ 'ae.console': {'handlers': ['console']}}}
393
474
  print(str(var_val))
394
475
 
395
476
  cae = AppBase('test_python_logging_params_dict_file', debug_level=DEBUG_LEVEL_DISABLED)
@@ -397,42 +478,24 @@ class TestPythonLogging:
397
478
 
398
479
  assert cae.py_log_params == var_val
399
480
 
400
- # logging.shutdown() TODO: encapsulate with _LOGGER = None into new method reset_logging() and add it to restore_app_env fixture ?!?!?
401
-
402
481
  # empty log file created by logging.config.dictConfig(py_logging_params) in :func:`ae.core.AppBase.init_logging`
403
482
  assert delete_files(log_file, ret_type='contents')[0] == ""
404
483
 
405
484
  def test_logging_params_dict_complex(self, restore_app_env):
406
- """ test logging with rotating file handler, first refactored migrated from
407
-
408
- TODO: investigate and fix the 4 commented out asserts in this test method
409
- .. which found log_text via `pjm check` pytest in console as well as in pycharm pytest run
410
- .. but in this test module (test_core.py) only the pjm/pytest run does not find log_text/log_files at all!
411
- .. or shows them accumulated in a later test
412
- Strange: a very similar test method did run fine with pjm&pycharm in ae_console/tests/test_console.py (v0.3.63)
413
-
414
- Looks like the problem lies in pytest (previous version done with caplog had also empty .text)
415
- .. but also in PyCharm, see:
416
-
417
- * https://stackoverflow.com/questions/59875983
418
- * https://github.com/pytest-dev/pytest/issues/3697
419
- * https://youtrack.jetbrains.com/issue/PY-48743/Running-Pytest-is-not-showing-logging-output
420
-
421
- """
485
+ """ test logging with rotating file handler. """
422
486
  log_file = 'test_py_log_complex.log'
423
487
  entry_prefix = "TEST LOG ENTRY "
424
488
 
425
- var_val = dict(version=1,
426
- disable_existing_loggers=False,
427
- handlers=dict(console={'class': 'logging.handlers.RotatingFileHandler',
428
- 'level': logging.INFO,
429
- 'filename': log_file,
430
- 'maxBytes': 33,
431
- 'backupCount': 63}),
432
- loggers={'root': dict(handlers=['console']),
433
- 'ae': dict(handlers=['console']),
434
- 'ae.core': dict(handlers=['console'])}
435
- )
489
+ var_val = {'version': 1,
490
+ 'disable_existing_loggers': False,
491
+ 'handlers': {'console': {'class': 'logging.handlers.RotatingFileHandler',
492
+ 'level': logging.INFO,
493
+ 'filename': log_file,
494
+ 'maxBytes': 33,
495
+ 'backupCount': 63}},
496
+ 'loggers': {'root': {'handlers': ['console']},
497
+ 'ae': {'handlers': ['console']},
498
+ 'ae.core': {'handlers': ['console']}}}
436
499
  print(str(var_val))
437
500
 
438
501
  cae = AppBase('test_python_logging_params_dict_file', debug_level=DEBUG_LEVEL_DISABLED)
@@ -503,6 +566,7 @@ class TestPythonLogging:
503
566
  logging.shutdown()
504
567
  # pjm: assert delete_files(log_file) == 0
505
568
  # pycharm: assert delete_files(log_file, ret_type='contents')[0].endswith(log_text + os.linesep)
569
+ assert delete_files(log_file, ret_type='contents')[0].endswith(log_text + os.linesep)
506
570
 
507
571
  try:
508
572
  log_text = entry_prefix + "4 error logging"
@@ -511,6 +575,7 @@ class TestPythonLogging:
511
575
  logging.shutdown()
512
576
  # pjm: assert delete_files(log_file) == 0
513
577
  # pycharm: assert delete_files(log_file, ret_type='contents')[0].endswith(log_text + os.linesep)
578
+ assert delete_files(log_file, ret_type='contents')[0].endswith(log_text + os.linesep)
514
579
 
515
580
  # loggers
516
581
  try:
@@ -520,6 +585,7 @@ class TestPythonLogging:
520
585
  logging.shutdown()
521
586
  # pjm: assert delete_files(log_file) == 0
522
587
  # pycharm: assert delete_files(log_file, ret_type='contents')[0].endswith(log_text + os.linesep)
588
+ assert delete_files(log_file, ret_type='contents')[0].endswith(log_text + os.linesep)
523
589
 
524
590
  try:
525
591
  log_text = entry_prefix + "4 error ae"
@@ -540,15 +606,16 @@ class TestPythonLogging:
540
606
  # AppBase.debug_out/.dpo
541
607
  sys.argv = ['tl_cdc'] # sys.argv has to be set to allow get_option('debug_level') calls done by debug_out()
542
608
  try:
543
- log_text = entry_prefix + "5 not logged dpo"
544
- cae.dpo(log_text, minimum_debug_level=DEBUG_LEVEL_DISABLED)
609
+ log_text = entry_prefix + "5 not logged debug_out/dpo"
610
+ cae.dpo(log_text)
545
611
  finally:
546
612
  logging.shutdown()
547
613
  assert delete_files(log_file) == 0
548
614
 
615
+ # AppBase.print_out/.po
549
616
  try:
550
- log_text = entry_prefix + "5 dpo"
551
- cae.dpo(log_text, minimum_debug_level=DEBUG_LEVEL_DISABLED, logger=ae_cae_logger)
617
+ log_text = entry_prefix + "5 print_out/po"
618
+ cae.po(log_text, logger=ae_cae_logger)
552
619
  finally:
553
620
  logging.shutdown()
554
621
  assert delete_files(log_file, ret_type='contents')[0].endswith(log_text + os.linesep)
@@ -579,7 +646,7 @@ class TestAppBase: # only some basic tests - test coverage is done by :clas
579
646
  app = AppBase(title, app_version=ver)
580
647
  assert app.app_title == title
581
648
  assert app.app_version == ver
582
- assert app.app_path == norm_path(os.path.dirname(sys.argv[0]))
649
+ assert app.app_path == norm_path(os_path_dirname(sys.argv[0]))
583
650
 
584
651
  def test_app_find_version(self, restore_app_env):
585
652
  app = AppBase()
@@ -604,7 +671,7 @@ class TestAppBase: # only some basic tests - test coverage is done by :clas
604
671
 
605
672
  _instance = _OtherClass()
606
673
  t_args = (1, 2, '3')
607
- t_kwargs = dict(a=1, b=2, c='3')
674
+ t_kwargs = {'a': 1, 'b': 2, 'c': '3'}
608
675
  assert app.call_method(_instance._method, *t_args, **t_kwargs) == (_instance, t_args, t_kwargs)
609
676
 
610
677
  def test_call_method_no_exception_if_not_exists(self, restore_app_env):
@@ -648,6 +715,12 @@ class TestAppBase: # only some basic tests - test coverage is done by :clas
648
715
  setattr(app, 'test_raiser', _raising_ex)
649
716
  app.call_method('test_raiser')
650
717
 
718
+ def test_debug_out(self, capsys, restore_app_env):
719
+ app = AppBase(debug_level=DEBUG_LEVEL_ENABLED)
720
+ tst = "tsT-debug-out-string"
721
+ app.debug_out(tst)
722
+ assert tst in capsys.readouterr()[0]
723
+
651
724
  def test_init_path_placeholders(self, restore_app_env):
652
725
  cae = AppBase("test_init_path_placeholders")
653
726
 
@@ -781,16 +854,46 @@ class TestAppBase: # only some basic tests - test coverage is done by :clas
781
854
  def test_app_instances_reset2(self):
782
855
  assert main_app_instance() is None
783
856
 
857
+ def test_shutdown_none(self, capsys, restore_app_env):
858
+ app = AppBase(debug_level=DEBUG_LEVEL_DISABLED)
859
+
860
+ with patch('ae.core.sys.exit', lambda *args, **kwargs: None):
861
+ app.shutdown(None)
862
+
863
+ out, err = capsys.readouterr()
864
+ assert out == ""
865
+ assert err == ""
866
+
867
+ def test_shutdown_0(self, capsys, restore_app_env):
868
+ app = AppBase(debug_level=DEBUG_LEVEL_ENABLED)
869
+
870
+ with patch('ae.core.sys.exit', lambda *args, **kwargs: None):
871
+ app.shutdown() # exit_code == 0
872
+
873
+ out, err = capsys.readouterr()
874
+ assert 'shutdown of ' in out
875
+ assert err == ""
876
+
877
+ def test_shutdown_1(self, capsys, restore_app_env):
878
+ app = AppBase(debug_level=DEBUG_LEVEL_VERBOSE)
879
+
880
+ with patch('ae.core.sys.exit', lambda *args, **kwargs: None):
881
+ app.shutdown(1)
882
+
883
+ assert 'shutdown of ' in capsys.readouterr()[0]
884
+
885
+ def test_shutdown_123456(self, capsys, restore_app_env):
886
+ app = AppBase()
887
+
888
+ with patch('ae.core.sys.exit', lambda *args, **kwargs: None):
889
+ app.shutdown(123456) # test warning if exit code is not in 0..255
890
+
891
+ assert 'extended exit code' in capsys.readouterr()[0]
892
+
784
893
  def test_verbose(self, capsys, restore_app_env):
785
894
  app = AppBase(debug_level=DEBUG_LEVEL_VERBOSE)
786
895
  assert app.verbose
787
896
 
788
- def test_debug_out(self, capsys, restore_app_env):
789
- app = AppBase(debug_level=DEBUG_LEVEL_ENABLED)
790
- tst = "tsT-debug-out-string"
791
- app.debug_out(tst)
792
- assert tst in capsys.readouterr()[0]
793
-
794
897
  def test_verbose_out(self, capsys, restore_app_env):
795
898
  app = AppBase(debug_level=DEBUG_LEVEL_ENABLED)
796
899
  tst = "tsT-debug-miss-string"
File without changes