ae-base 0.3.47__tar.gz → 0.3.49__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,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: ae_base
3
- Version: 0.3.47
3
+ Version: 0.3.49
4
4
  Summary: ae namespace module portion base: basic constants, helper functions and context manager
5
5
  Home-page: https://gitlab.com/ae-group/ae_base
6
6
  Author: AndiEcker
@@ -53,20 +53,32 @@ Requires-Dist: typing; extra == "tests"
53
53
  Requires-Dist: types-setuptools; extra == "tests"
54
54
  Requires-Dist: wheel; extra == "tests"
55
55
  Requires-Dist: twine; extra == "tests"
56
+ Dynamic: author
57
+ Dynamic: author-email
58
+ Dynamic: classifier
59
+ Dynamic: description
60
+ Dynamic: description-content-type
61
+ Dynamic: home-page
62
+ Dynamic: keywords
63
+ Dynamic: license
64
+ Dynamic: project-url
65
+ Dynamic: provides-extra
66
+ Dynamic: requires-python
67
+ Dynamic: summary
56
68
 
57
69
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.95 -->
58
70
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
59
- # base 0.3.47
71
+ # base 0.3.49
60
72
 
61
73
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
62
74
  https://gitlab.com/ae-group/ae_base)
63
75
  [![LatestPyPIrelease](
64
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.46?logo=python)](
65
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.46)
76
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.48?logo=python)](
77
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.48)
66
78
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
67
79
  https://pypi.org/project/ae-base/#history)
68
80
 
69
- >ae_base module 0.3.47.
81
+ >ae_base module 0.3.49.
70
82
 
71
83
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
72
84
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -1,16 +1,16 @@
1
1
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.95 -->
2
2
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
3
- # base 0.3.47
3
+ # base 0.3.49
4
4
 
5
5
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
6
6
  https://gitlab.com/ae-group/ae_base)
7
7
  [![LatestPyPIrelease](
8
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.46?logo=python)](
9
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.46)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.48?logo=python)](
9
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.48)
10
10
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
11
11
  https://pypi.org/project/ae-base/#history)
12
12
 
13
- >ae_base module 0.3.47.
13
+ >ae_base module 0.3.49.
14
14
 
15
15
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
16
16
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -5,6 +5,11 @@ basic constants, helper functions and context manager
5
5
  this module is pure python, has no external dependencies, and is providing base constants, common helper
6
6
  functions, useful classes and context managers.
7
7
 
8
+ .. note::
9
+ on import of this module, while running on Android OS, it will monkey patch the :mod:`shutil` module
10
+ to allow to use them on Android devices. therefore the import of this module should be one of the first ones
11
+ in your app's main module.
12
+
8
13
 
9
14
  base constants
10
15
  --------------
@@ -20,10 +25,6 @@ default values to compile file and folder names for a package or an app project
20
25
  :data:`PACKAGE_INCLUDE_FILES_PREFIX`, :data:`PY_EXT`, :data:`PY_INIT`, :data:`PY_MAIN`, :data:`CFG_EXT`
21
26
  and :data:`INI_EXT`.
22
27
 
23
- the constants :data:`PACKAGE_NAME`, :data:`PACKAGE_DOMAIN` and :data:`PERMISSIONS` are mainly
24
- used for apps running on mobile devices. to avoid redundancies, these values get loaded from the
25
- :data:`build config file <BUILD_CONFIG_FILE>` - if it exists in the current working directory.
26
-
27
28
  with the help of the format string constant :data:`NOW_STR_FORMAT` and the function :func:`now_str` you can create a
28
29
  sortable and compact string from a timestamp.
29
30
 
@@ -82,31 +83,13 @@ variables for your application are :func:`sys_env_dict` and :func:`sys_env_text`
82
83
  to integrate system environment variables from ``.env`` files into :data:`os.environ` the helper functions
83
84
  :func:parse_dotenv`, :func:`load_env_var_defaults` and :func:`load_dotenvs` are provided.
84
85
 
86
+ the :mod:`ae.core` portion is providing more OS-specific constants and helper functions, like e.g.
87
+ :func:`start_app_service` and :func:`request_app_permissions`.
85
88
 
86
- android-specific constants and helper functions
87
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
88
-
89
- some helper functions of this module are provided to be used for the Android OS.
90
-
91
- the helper function :func:`start_app_service` is starting a service in its own, separate thread.
92
- with the function :func:`request_app_permissions` you can ensure that all your Android permissions will be
93
- requested. the module :mod:`ae.kivy.apps` does this automatically on app startup. on other platforms than
94
- Android it will have no effect to call these functions.
95
-
96
- .. note:: importing this module on an Android OS, it is monkey patching the :mod:`shutil` module to prevent crashes.
97
-
98
- links to other android code and service examples and documentation:
99
-
100
- * `https://python-for-android.readthedocs.io/en/latest/`__
101
- * `https://github.com/kivy/python-for-android/tree/develop/pythonforandroid/recipes/android/src/android`__
102
- * `https://github.com/tshirtman/kivy_service_osc/blob/master/src/main.py`__
103
- * `https://blog.kivy.org/2014/01/building-a-background-application-on-android-with-kivy/`__
104
- * `https://github.com/Android-for-Python/Android-for-Python-Users`__
105
- * `https://github.com/Android-for-Python/INDEX-of-Examples`__
106
-
107
- big thanks to `Robert Flatt <https://github.com/RobertFlatt>`__ for his investigations, findings and documentations to
108
- code and build Kivy apps for the Android OS, and to `Gabriel Pettier <https://github.com/tshirtman>`__ for his service
109
- osc example.
89
+ .. note::
90
+ on import of this module, while running on Android OS, it will monkey patch the :mod:`shutil` module to allow to
91
+ use them on Android devices, and on first app start request the permissions of your app. therefore to prevent
92
+ permission errors, the import of this module should be the first statement in the main module of your app.
110
93
 
111
94
 
112
95
  types, classes and mixins
@@ -117,6 +100,9 @@ allowing also ``None`` is an accepted argument value.
117
100
 
118
101
  to extend any class with an intelligent error message handling, add the mixin :class:`ErrorMsgMixin` to it.
119
102
 
103
+ the classes :class:`UnformattedValue` and :class:`GivenFormatter` can be used to format strings with placeholders
104
+ enclosed in curly brackets. the function :func:`format_given` is using them to format templates with placeholders.
105
+
120
106
 
121
107
  generic context manager
122
108
  -----------------------
@@ -149,7 +135,7 @@ another useful helper function provided by this portion to inspect and debug you
149
135
  os.path shortcuts
150
136
  -----------------
151
137
 
152
- the following data items are pointers to shortcut the lookup to their related functions in the
138
+ the following data items are pointers to shortcut at runtime the lookup to their related functions in the
153
139
  Python module :mod:`os.path`:
154
140
  """
155
141
  import datetime
@@ -161,6 +147,7 @@ import platform
161
147
  import re
162
148
  import shutil
163
149
  import socket
150
+ import string
164
151
  import sys
165
152
  import unicodedata
166
153
  import warnings
@@ -173,7 +160,7 @@ from types import ModuleType
173
160
  from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
174
161
 
175
162
 
176
- __version__ = '0.3.47'
163
+ __version__ = '0.3.49'
177
164
 
178
165
 
179
166
  os_path_abspath = os.path.abspath
@@ -248,8 +235,7 @@ NOW_STR_FORMAT = "{sep}%Y%m%d{sep}%H%M%S{sep}%f" #: timestamp format of :func
248
235
  SKIPPED_MODULES = ('ae.base', 'ae.paths', 'ae.dynamicod', 'ae.core', 'ae.console', 'ae.gui_app', 'ae.gui_help',
249
236
  'ae.kivy', 'ae.kivy.apps', 'ae.kivy.behaviors', 'ae.kivy.i18n', 'ae.kivy.tours', 'ae.kivy.widgets',
250
237
  'ae.enaml_app', 'ae.beeware_app', 'ae.pyglet_app', 'ae.pygobject_app', 'ae.dabo_app',
251
- 'ae.qpython_app', 'ae.appjar_app', # removed in V 0.1.4: 'ae.lisz_app_data',
252
- 'importlib._bootstrap', 'importlib._bootstrap_external')
238
+ 'ae.qpython_app', 'ae.appjar_app', 'importlib._bootstrap', 'importlib._bootstrap_external')
253
239
  """ skipped modules used as default by :func:`module_name`, :func:`stack_var` and :func:`stack_vars` """
254
240
 
255
241
 
@@ -487,6 +473,50 @@ def force_encoding(text: Union[str, bytes], encoding: str = DEF_ENCODING, errors
487
473
  return enc_str.decode(encoding=encoding)
488
474
 
489
475
 
476
+ class UnformattedValue:
477
+ """ helper class for :func:`~ae.base.format_given` to keep placeholder with format unchanged if not found. """
478
+ def __init__(self, key: str):
479
+ self.key = key
480
+
481
+ def __format__(self, format_spec: str):
482
+ """ overriding Python object class method to return placeholder unchanged including the curly brackets. """
483
+ return "{{{}{}}}".format(self.key, ":" + format_spec if format_spec else "")
484
+
485
+
486
+ class GivenFormatter(string.Formatter):
487
+ """ helper class for :func:`~ae.base.format_given` to keep placeholder with format unchanged if not found. """
488
+ def get_value(self, key, args, kwargs):
489
+ """ overriding to keep placeholder unchanged if not found """
490
+ try:
491
+ return super().get_value(key, args, kwargs)
492
+ except KeyError:
493
+ return UnformattedValue(key)
494
+
495
+
496
+ def format_given(text: str, placeholder_map: dict[str, Any], strict: bool = False):
497
+ """ replacement for Python's str.format_map(), keeping intact placeholders that are not in the specified mapping.
498
+
499
+ :param text: text/template in which the given/specified placeholders will get replaced. in contrary
500
+ to str.format_map() no KeyError will be raised for placeholders not specified in
501
+ :paramref:`~format_given.placeholder_map`.
502
+ :param placeholder_map: dict with placeholder keys to be replaced in :paramref:`~format_given.text` argument.
503
+ :param strict: pass True to raise error for text templates containing unpaired curly brackets.
504
+ :return: the specified :paramref:`~format_given.text` with only the placeholders specified in
505
+ :paramref:`~format_given.placeholder_map` replaced with their respective map value.
506
+ additionally any ValueError that would be thrown by str.format_map(), e.g. if the
507
+
508
+
509
+ inspired by the answer of CodeManX in `https://stackoverflow.com/questions/3536303`__
510
+ """
511
+ formatter = GivenFormatter()
512
+ try:
513
+ return formatter.vformat(text, (), placeholder_map)
514
+ except (ValueError, Exception) as ex:
515
+ if strict:
516
+ raise ex
517
+ return text
518
+
519
+
490
520
  def full_stack_trace(ex: Exception) -> str:
491
521
  """ get full stack trace from an exception.
492
522
 
@@ -758,7 +788,7 @@ def os_host_name() -> str:
758
788
 
759
789
  :return: machine name string.
760
790
  """
761
- return defuse(platform.node()) or "undeterminableHostName"
791
+ return defuse(platform.node()) or "indeterminableHostName"
762
792
 
763
793
 
764
794
  def os_local_ip() -> str:
@@ -1190,23 +1220,6 @@ class ErrorMsgMixin:
1190
1220
  self._err_msg = ""
1191
1221
 
1192
1222
 
1193
- # package and permissions handling defaults for all other platforms and frameworks
1194
- PACKAGE_NAME = stack_var('__name__') or 'unspecified_package' #: package name default
1195
- PACKAGE_DOMAIN = 'org.test' #: package domain default
1196
- PERMISSIONS = "INTERNET, VIBRATE, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE" #: permissions default
1197
- if os_path_isfile(BUILD_CONFIG_FILE): # pragma: no cover
1198
- PACKAGE_NAME, PACKAGE_DOMAIN, PERMISSIONS = build_config_variable_values(
1199
- ('package.name', PACKAGE_NAME),
1200
- ('package.domain', PACKAGE_DOMAIN),
1201
- ('android.permissions', PERMISSIONS))
1202
- elif os_platform == 'android': # pragma: no cover
1203
- _importing_package = norm_path(stack_var('__file__') or 'empty_package' + PY_EXT)
1204
- if os_path_basename(_importing_package) in (PY_INIT, PY_MAIN):
1205
- _importing_package = os_path_dirname(_importing_package)
1206
- _importing_package = os_path_splitext(os_path_basename(_importing_package))[0]
1207
- write_file(f'{_importing_package}_debug.log', f"{BUILD_CONFIG_FILE} not bundled - using defaults\n", extra_mode='a')
1208
-
1209
-
1210
1223
  if os_platform == 'android': # pragma: no cover
1211
1224
  # monkey patch the :func:`shutil.copystat` and :func:`shutil.copymode` helper functions, which are crashing on
1212
1225
  # 'android' (see # `<https://bugs.python.org/issue28141>`__ and `<https://bugs.python.org/issue32073>`__). these
@@ -1216,41 +1229,3 @@ if os_platform == 'android': # pragma: no cov
1216
1229
  # on the destination root directory.
1217
1230
  shutil.copymode = dummy_function
1218
1231
  shutil.copystat = dummy_function
1219
-
1220
- # import permissions module from python-for-android (recipes/android/src/android/permissions.py)
1221
- # noinspection PyUnresolvedReferences
1222
- from android.permissions import request_permissions, Permission # type: ignore # pylint: disable=import-error
1223
- from jnius import autoclass # type: ignore
1224
-
1225
- def request_app_permissions(callback: Optional[Callable[[List[Permission], List[bool]], None]] = None):
1226
- """ request app/service permissions on Android OS.
1227
-
1228
- :param callback: optional callback receiving two list arguments with identical length,
1229
- the 1st with the requested permissions and
1230
- the 2nd with booleans stating if the permission got granted (True) or rejected (False).
1231
- """
1232
- permissions = []
1233
- for permission_str in PERMISSIONS.split(','):
1234
- permission = getattr(Permission, permission_str.strip(), None)
1235
- if permission:
1236
- permissions.append(permission)
1237
- request_permissions(permissions, callback=callback)
1238
-
1239
- def start_app_service(service_arg: str = "") -> Any:
1240
- """ start service.
1241
-
1242
- :param service_arg: string value to be assigned to environment variable PYTHON_SERVICE_ARGUMENT on start.
1243
- :return: service instance.
1244
-
1245
- see `<https://github.com/tshirtman/kivy_service_osc/blob/master/src/main.py>`__
1246
- and `<https://python-for-android.readthedocs.io/en/latest/services/#arbitrary-scripts-services>`__
1247
- """
1248
- service_instance = autoclass(f"{PACKAGE_DOMAIN}.{PACKAGE_NAME}.Service{PACKAGE_NAME.capitalize()}")
1249
- activity = autoclass('org.kivy.android.PythonActivity').mActivity
1250
- service_instance.start(activity, service_arg) # service_arg will be in env var PYTHON_SERVICE_ARGUMENT
1251
-
1252
- return service_instance
1253
-
1254
- else:
1255
- request_app_permissions = dummy_function
1256
- start_app_service = dummy_function
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: ae_base
3
- Version: 0.3.47
3
+ Version: 0.3.49
4
4
  Summary: ae namespace module portion base: basic constants, helper functions and context manager
5
5
  Home-page: https://gitlab.com/ae-group/ae_base
6
6
  Author: AndiEcker
@@ -53,20 +53,32 @@ Requires-Dist: typing; extra == "tests"
53
53
  Requires-Dist: types-setuptools; extra == "tests"
54
54
  Requires-Dist: wheel; extra == "tests"
55
55
  Requires-Dist: twine; extra == "tests"
56
+ Dynamic: author
57
+ Dynamic: author-email
58
+ Dynamic: classifier
59
+ Dynamic: description
60
+ Dynamic: description-content-type
61
+ Dynamic: home-page
62
+ Dynamic: keywords
63
+ Dynamic: license
64
+ Dynamic: project-url
65
+ Dynamic: provides-extra
66
+ Dynamic: requires-python
67
+ Dynamic: summary
56
68
 
57
69
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.95 -->
58
70
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
59
- # base 0.3.47
71
+ # base 0.3.49
60
72
 
61
73
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
62
74
  https://gitlab.com/ae-group/ae_base)
63
75
  [![LatestPyPIrelease](
64
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.46?logo=python)](
65
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.46)
76
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.48?logo=python)](
77
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.48)
66
78
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
67
79
  https://pypi.org/project/ae-base/#history)
68
80
 
69
- >ae_base module 0.3.47.
81
+ >ae_base module 0.3.49.
70
82
 
71
83
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
72
84
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -21,7 +21,7 @@ from ae.base import (
21
21
  UNSET,
22
22
  URI_SEP_CHAR, app_name_guess, build_config_variable_values, camel_to_snake, deep_dict_update, dummy_function,
23
23
  duplicates, env_str,
24
- dedefuse, force_encoding, full_stack_trace, import_module, instantiate_config_parser, in_wd,
24
+ dedefuse, force_encoding, format_given, full_stack_trace, import_module, instantiate_config_parser, in_wd,
25
25
  load_env_var_defaults, load_dotenvs, main_file_paths_parts, module_attr, module_file_path, module_name,
26
26
  norm_line_sep, norm_name, norm_path, now_str, os_host_name, os_local_ip, _os_platform, os_user_name,
27
27
  parse_dotenv, project_main_file, read_file, round_traditional, snake_to_camel, stack_frames, stack_var, stack_vars,
@@ -86,15 +86,18 @@ class TestErrorMsgMixin:
86
86
  class _AppMock(ErrorMsgMixin):
87
87
  cae = None
88
88
 
89
- def po(self):
89
+ @staticmethod
90
+ def po():
90
91
  """ po() mock """
91
92
  return "po"
92
93
 
93
- def dpo(self):
94
+ @staticmethod
95
+ def dpo():
94
96
  """ dpo() mock """
95
97
  return "dpo"
96
98
 
97
- def vpo(self):
99
+ @staticmethod
100
+ def vpo():
98
101
  """ vpo() mock """
99
102
  return "vpo"
100
103
 
@@ -323,6 +326,27 @@ class TestBaseHelpers:
323
326
  with pytest.raises(TypeError):
324
327
  assert force_encoding(s, encoding=cast(str, None)) == '\\xe4\\xf6\\xfc'
325
328
 
329
+ def test_format_given(self):
330
+ assert format_given("test text with {placeholder}", {}) == "test text with {placeholder}"
331
+ assert format_given("test text with {placeholder:.2e}", {}) == "test text with {placeholder:.2e}"
332
+ assert format_given("a {placeholder} {{test}}", {}) == "a {placeholder} {test}"
333
+
334
+ assert format_given("test text with {placeholder}", {'placeholder': "replaced"}) == "test text with replaced"
335
+ assert format_given("test text with {placeholder:.2e}", {'placeholder': 3.14159}) == "test text with 3.14e+00"
336
+
337
+ assert format_given("a {ph} {{test}}", {'ph': "rep"}) == "a rep {test}"
338
+ assert format_given("a {{ph}} {test}", {'ph': "rep"}) == "a {ph} {test}"
339
+ assert format_given("a {{ph} {test}}", {'ph': "rep"}) == "a {{ph} {test}}"
340
+
341
+ assert format_given("a non-ph}", {'ph': "rep"}) == "a non-ph}"
342
+ assert format_given("a non-{ph", {'ph': "rep"}) == "a non-{ph"
343
+
344
+ def test_format_given_err(self):
345
+ with pytest.raises(ValueError):
346
+ format_given("test text with {placeholder", {}, strict=True) # expected '}' before end of string
347
+ with pytest.raises(ValueError):
348
+ format_given("test text with placeholder}", {}, strict=True) # Single '}' encountered in format string
349
+
326
350
  def test_import_module_ae_base(self):
327
351
  mod_ref = import_module('ae.base')
328
352
  assert isinstance(mod_ref, ModuleType)
File without changes
File without changes
File without changes