ae-base 0.3.40__tar.gz → 0.3.42__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.tpl_project V0.3.30 -->
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_project V0.3.31 -->
2
2
  ### GNU GENERAL PUBLIC LICENSE
3
3
 
4
4
  Version 3, 29 June 2007
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ae_base
3
- Version: 0.3.40
3
+ Version: 0.3.42
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
@@ -32,6 +32,7 @@ Requires-Dist: mypy; extra == "dev"
32
32
  Requires-Dist: pylint; extra == "dev"
33
33
  Requires-Dist: pytest; extra == "dev"
34
34
  Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: pytest-django; extra == "dev"
35
36
  Requires-Dist: typing; extra == "dev"
36
37
  Requires-Dist: types-setuptools; extra == "dev"
37
38
  Requires-Dist: wheel; extra == "dev"
@@ -46,6 +47,7 @@ Requires-Dist: mypy; extra == "tests"
46
47
  Requires-Dist: pylint; extra == "tests"
47
48
  Requires-Dist: pytest; extra == "tests"
48
49
  Requires-Dist: pytest-cov; extra == "tests"
50
+ Requires-Dist: pytest-django; extra == "tests"
49
51
  Requires-Dist: typing; extra == "tests"
50
52
  Requires-Dist: types-setuptools; extra == "tests"
51
53
  Requires-Dist: wheel; extra == "tests"
@@ -53,17 +55,17 @@ Requires-Dist: twine; extra == "tests"
53
55
 
54
56
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
55
57
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
56
- # base 0.3.40
58
+ # base 0.3.42
57
59
 
58
60
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
59
61
  https://gitlab.com/ae-group/ae_base)
60
62
  [![LatestPyPIrelease](
61
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.39?logo=python)](
62
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.39)
63
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.41?logo=python)](
64
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.41)
63
65
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
64
66
  https://pypi.org/project/ae-base/#history)
65
67
 
66
- >ae_base module 0.3.40.
68
+ >ae_base module 0.3.42.
67
69
 
68
70
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
69
71
  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.94 -->
2
2
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
3
- # base 0.3.40
3
+ # base 0.3.42
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.39?logo=python)](
9
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.39)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.41?logo=python)](
9
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.41)
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.40.
13
+ >ae_base module 0.3.42.
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)
@@ -31,6 +31,9 @@ sortable and compact string from a timestamp.
31
31
  base helper functions
32
32
  ---------------------
33
33
 
34
+ :func:`now_str` creates a timestamp string with the actual UTC date and time. the :func:`utc_datetime` provides the
35
+ actual UTC date and time as datetime object.
36
+
34
37
  to write more compact and readable code for the most common file I/O operations, the helper functions :func:`read_file`
35
38
  and :func:`write_file` are wrapping Python's built-in :func:`open` function and its context manager.
36
39
 
@@ -43,6 +46,9 @@ the function :func:`duplicates` returns the duplicates of an iterable type.
43
46
  to normalize a file path, in order to remove `.`, `..` placeholders, to resolve symbolic links or to make it relative or
44
47
  absolute, call the function :func:`norm_path`.
45
48
 
49
+ :func:`uri2filename` converts special characters of a URI/URL resulting in a string that can be used as a file name.
50
+ use the function :func:`filename2uri` to convert this string back to the corresponding URL/URI.
51
+
46
52
  :func:`camel_to_snake` and :func:`snake_to_camel` providing name conversions of class and method names.
47
53
 
48
54
  to encode Unicode strings to other codecs the functions :func:`force_encoding` and :func:`to_ascii` can be used.
@@ -137,6 +143,13 @@ to determine e.g. variable values of the callers of a function/method.
137
143
  :attr:`title <AppBase.app_title>` of an application, if these values are not specified in the instance initializer.
138
144
 
139
145
  another useful helper function provided by this portion to inspect and debug your code is :func:`full_stack_trace`.
146
+
147
+
148
+ os.path shortcuts
149
+ -----------------
150
+
151
+ the following data items are pointers to shortcut the lookup to their related functions in the
152
+ Python module :mod:`os.path`:
140
153
  """
141
154
  import datetime
142
155
  import getpass
@@ -160,7 +173,21 @@ from types import ModuleType
160
173
  from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
161
174
 
162
175
 
163
- __version__ = '0.3.40'
176
+ __version__ = '0.3.42'
177
+
178
+
179
+ os_path_abspath = os.path.abspath
180
+ os_path_basename = os.path.basename
181
+ os_path_dirname = os.path.dirname
182
+ os_path_expanduser = os.path.expanduser
183
+ os_path_isdir = os.path.isdir
184
+ os_path_isfile = os.path.isfile
185
+ os_path_join = os.path.join
186
+ os_path_normpath = os.path.normpath
187
+ os_path_realpath = os.path.realpath
188
+ os_path_relpath = os.path.relpath
189
+ os_path_sep = os.path.sep
190
+ os_path_splitext = os.path.splitext
164
191
 
165
192
 
166
193
  DOCS_FOLDER = 'docs' #: project documentation root folder name
@@ -250,10 +277,10 @@ def app_name_guess() -> str:
250
277
  if not app_name:
251
278
  unspecified_app_names = ('ae_base', 'app', '_jb_pytest_runner', 'main', '__main__', 'pydevconsole', 'src')
252
279
  path = sys.argv[0]
253
- app_name = os.path.splitext(os.path.basename(path))[0]
280
+ app_name = os_path_splitext(os_path_basename(path))[0]
254
281
  if app_name.lower() in unspecified_app_names:
255
282
  path = os.getcwd()
256
- app_name = os.path.basename(path)
283
+ app_name = os_path_basename(path)
257
284
  if app_name.lower() in unspecified_app_names:
258
285
  app_name = "unguessable"
259
286
  return app_name
@@ -267,7 +294,7 @@ def build_config_variable_values(*names_defaults: Tuple[str, Any], section: str
267
294
  :return: tuple of build config variable values (using the passed default value if not specified
268
295
  in the :data:`BUILD_CONFIG_FILE` spec file or if the spec file does not exist in cwd).
269
296
  """
270
- if not os.path.exists(BUILD_CONFIG_FILE):
297
+ if not os_path_isfile(BUILD_CONFIG_FILE):
271
298
  return tuple(def_val for name, def_val in names_defaults)
272
299
 
273
300
  config = instantiate_config_parser()
@@ -352,6 +379,17 @@ def env_str(name: str, convert_name: bool = False) -> Optional[str]:
352
379
  return os.environ.get(name)
353
380
 
354
381
 
382
+ def filename2uri(file_name: str) -> str:
383
+ """ convert a file name converted by :func:`uri2filename` back to its representation as a URI
384
+
385
+ :param file_name: name of the file/folder to convert back to its URI representation.
386
+ :return: URI string.
387
+
388
+ .. hint:: to ensure proper conversion the specified file name has to be created by :func:`uri2filename`.
389
+ """
390
+ return urllib.parse.unquote(file_name)
391
+
392
+
355
393
  def force_encoding(text: Union[str, bytes], encoding: str = DEF_ENCODING, errors: str = DEF_ENCODE_ERRORS) -> str:
356
394
  """ force/ensure the encoding of text (str or bytes) without any UnicodeDecodeError/UnicodeEncodeError.
357
395
 
@@ -400,8 +438,8 @@ def import_module(import_name: str, path: Optional[Union[str, UnsetType]] = UNSE
400
438
  :return: a reference to the loaded module or ``None`` if module could not be imported.
401
439
  """
402
440
  if path is UNSET:
403
- path = import_name.replace('.', os.path.sep)
404
- path += PY_EXT if os.path.isfile(path + PY_EXT) else os.path.sep + PY_INIT
441
+ path = import_name.replace('.', os_path_sep)
442
+ path += PY_EXT if os_path_isfile(path + PY_EXT) else os_path_sep + PY_INIT
405
443
  mod_ref = None
406
444
 
407
445
  spec = importlib.util.spec_from_file_location(import_name, path) # type: ignore # silly mypy
@@ -452,7 +490,7 @@ def load_dotenvs():
452
490
  """
453
491
  load_env_var_defaults(os.getcwd())
454
492
  if file_name := stack_var('__file__'):
455
- load_env_var_defaults(os.path.dirname(os.path.abspath(file_name)))
493
+ load_env_var_defaults(os_path_dirname(os_path_abspath(file_name)))
456
494
 
457
495
 
458
496
  def load_env_var_defaults(start_dir: str):
@@ -468,18 +506,18 @@ def load_env_var_defaults(start_dir: str):
468
506
  only variables that are not declared in :data:`os.environ` will be added (with the
469
507
  value specified in the ``.env`` file to be loaded).
470
508
  """
471
- file_path = os.path.abspath(os.path.join(start_dir, DOTENV_FILE_NAME))
472
- if not os.path.isfile(file_path):
473
- file_path = os.path.join(os.path.dirname(start_dir), DOTENV_FILE_NAME)
509
+ file_path = os_path_abspath(os_path_join(start_dir, DOTENV_FILE_NAME))
510
+ if not os_path_isfile(file_path):
511
+ file_path = os_path_join(os_path_dirname(start_dir), DOTENV_FILE_NAME)
474
512
 
475
- while os.path.isfile(file_path):
513
+ while os_path_isfile(file_path):
476
514
  for var_nam, var_val in parse_dotenv(file_path).items():
477
515
  if var_nam not in os.environ:
478
516
  os.environ[var_nam] = var_val
479
517
 
480
518
  if os.sep not in file_path:
481
519
  break # pragma: no cover # prevent endless-loop for ``.env`` file in root dir (os.sep == '/')
482
- file_path = os.path.join(os.path.dirname(os.path.dirname(file_path)), DOTENV_FILE_NAME)
520
+ file_path = os_path_join(os_path_dirname(os_path_dirname(file_path)), DOTENV_FILE_NAME)
483
521
 
484
522
 
485
523
  def main_file_paths_parts(portion_name: str) -> Tuple[Tuple[str, ...], ...]:
@@ -603,20 +641,20 @@ def norm_path(path: str, make_absolute: bool = True, remove_base_path: str = "",
603
641
  """
604
642
  path = path or "."
605
643
  if path[0] == "~":
606
- path = os.path.expanduser(path)
644
+ path = os_path_expanduser(path)
607
645
 
608
646
  if remove_dots:
609
- path = os.path.normpath(path)
647
+ path = os_path_normpath(path)
610
648
 
611
649
  if resolve_sym_links:
612
- path = os.path.realpath(path)
650
+ path = os_path_realpath(path)
613
651
  elif make_absolute:
614
- path = os.path.abspath(path)
652
+ path = os_path_abspath(path)
615
653
 
616
654
  if remove_base_path:
617
655
  if remove_base_path[0] == "~":
618
- remove_base_path = os.path.expanduser(remove_base_path)
619
- path = os.path.relpath(path, remove_base_path)
656
+ remove_base_path = os_path_expanduser(remove_base_path)
657
+ path = os_path_relpath(path, remove_base_path)
620
658
 
621
659
  return path
622
660
 
@@ -628,10 +666,7 @@ def now_str(sep: str = "") -> str:
628
666
  the seconds from the microseconds).
629
667
  :return: naive UTC timestamp (without timezone info) as string (length=20 + 3 * len(sep)).
630
668
  """
631
- # if sys.version_info >= (3, 12):
632
- return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None).strftime(NOW_STR_FORMAT.format(sep=sep))
633
- # else:
634
- # return datetime.datetime.utcnow().strftime(NOW_STR_FORMAT.format(sep=sep))
669
+ return utc_datetime().strftime(NOW_STR_FORMAT.format(sep=sep))
635
670
 
636
671
 
637
672
  def os_host_name() -> str:
@@ -754,23 +789,22 @@ def project_main_file(import_name: str, project_path: str = "") -> str:
754
789
  sister project (under the same project parent folder).
755
790
  :return: absolute file path/name of main module or empty string if no main/version file found.
756
791
  """
757
- join = os.path.join
758
792
  *namespace_dirs, portion_name = import_name.split('.')
759
793
  project_name = ('_'.join(namespace_dirs) + '_' if namespace_dirs else "") + portion_name
760
794
  paths_parts = main_file_paths_parts(portion_name)
761
795
 
762
796
  project_path = norm_path(project_path)
763
797
  module_paths = []
764
- if os.path.basename(project_path) != project_name:
765
- module_paths.append(join(os.path.dirname(project_path), project_name, *namespace_dirs))
798
+ if os_path_basename(project_path) != project_name:
799
+ module_paths.append(os_path_join(os_path_dirname(project_path), project_name, *namespace_dirs))
766
800
  if namespace_dirs:
767
- module_paths.append(join(project_path, *namespace_dirs))
801
+ module_paths.append(os_path_join(project_path, *namespace_dirs))
768
802
  module_paths.append(project_path)
769
803
 
770
804
  for module_path in module_paths:
771
805
  for path_parts in paths_parts:
772
- main_file = join(module_path, *path_parts)
773
- if os.path.isfile(main_file):
806
+ main_file = os_path_join(module_path, *path_parts)
807
+ if os_path_isfile(main_file):
774
808
  return main_file
775
809
  return ""
776
810
 
@@ -885,7 +919,7 @@ def stack_vars(*skip_modules: str,
885
919
  :param max_depth: the maximum depth in the call stack from which to return the variables. if the specified
886
920
  argument is not zero and no :paramref:`~stack_vars.skip_modules` are specified then the
887
921
  first deeper stack frame that is not within the default :data:`SKIPPED_MODULES` will be
888
- returned. if this argument and :paramref:`~stack_var.find_name` get not passed then the
922
+ returned. if this argument and :paramref:`~stack_vars.find_name` get not passed then the
889
923
  variables of the top stack frame will be returned.
890
924
  :return: tuple of the global and local variable dicts and the depth in the call stack.
891
925
  """
@@ -986,33 +1020,38 @@ def uri2filename(uri: str) -> str:
986
1020
 
987
1021
  more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
988
1022
  and of Christopher Oezbek on https://stackoverflow.com/questions/1976007.
1023
+
1024
+ .. hint:: use :func:`filename2uri` to convert the resulting file name back to the corresponding URO
989
1025
  """
990
1026
  # using urllib.parse.quote(uri, safe="") instead would convert also any non-ascii (e.g. umlaut) characters into hex
991
1027
  # added [] to str.join() argument because List comprehensions are faster than generator expressions
992
1028
  return "".join([f"%{hex(ord(_))[2:].upper()}" if _ in '/|\\:*?"<>%' else _ for _ in uri])
993
1029
 
994
1030
 
995
- def filename2uri(file_name: str) -> str:
996
- """ convert a file name converted by :func:`uri2filename` back to its representation as a URI
1031
+ def utc_datetime() -> datetime.datetime:
1032
+ """ return the current UTC timestamp as string (to use as suffix for file and variable/attribute names).
997
1033
 
998
- :param file_name: name of the file/folder to convert back to its URI representation.
999
- :return: URI string.
1034
+ :return: timestamp string of the actual UTC date and time.
1000
1035
  """
1001
- return urllib.parse.unquote(file_name)
1036
+ return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
1002
1037
 
1003
1038
 
1004
1039
  def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "", encoding: Optional[str] = None):
1005
1040
  """ (over)write the file specified by :paramref:`~write_file.file_path` with text or binary/bytes content.
1006
1041
 
1007
1042
  :param file_path: file path/name to write the passed content into (overwriting any previous content!).
1008
- :param content: new file content either passed as string or list of line strings (will be
1009
- concatenated with the line separator of the current OS: os.linesep).
1010
- :param extra_mode: open mode flag characters. passed unchanged to the `mode` argument of :func:`open` if
1011
- this argument starts with 'a', else this argument value will be appended to 'w'.
1043
+ :param content: new file content passed either as string or bytes array. if a bytes array get passed
1044
+ then this method will automatically write the content as binary.
1045
+ :param extra_mode: additional open mode flag characters. passed to the `mode` argument of :func:`open` if
1046
+ this argument starts with 'a' or 'w', else this argument value will be appended to 'w'
1047
+ before it get passed to the `mode` argument of :func:`open`.
1048
+ if the :paramref:`~write_file.content` is a bytes array, then a 'b' character will
1049
+ be automatically added to the `mode` argument of :func:`open` (if not already specified
1050
+ in this argument).
1012
1051
  :param encoding: encoding used to write/convert/interpret the file content to write.
1013
1052
  :raises FileExistsError: if file exists already and is write-protected.
1014
1053
  :raises FileNotFoundError: if parts of the file path do not exist.
1015
- :raises OSError: if :paramref:`~read_file.file_path` is misspelled or contains invalid characters.
1054
+ :raises OSError: if :paramref:`~write_file.file_path` is misspelled or contains invalid characters.
1016
1055
  :raises PermissionError: if current OS user account lacks permissions to read the file content.
1017
1056
  :raises ValueError: on decoding errors.
1018
1057
 
@@ -1025,7 +1064,13 @@ def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "",
1025
1064
  for an intent result.
1026
1065
  Related german docs: https://developer.android.com/training/data-storage/shared/media?hl=de
1027
1066
  """
1028
- with open(file_path, ('' if extra_mode.startswith('a') else 'w') + extra_mode, encoding=encoding) as file_handle:
1067
+ if isinstance(content, bytes) and 'b' not in extra_mode:
1068
+ extra_mode += 'b'
1069
+
1070
+ if extra_mode == '' or extra_mode[0] not in ('a', 'w'):
1071
+ extra_mode = 'w' + extra_mode
1072
+
1073
+ with open(file_path, mode=extra_mode, encoding=encoding) as file_handle:
1029
1074
  file_handle.write(content)
1030
1075
 
1031
1076
 
@@ -1054,16 +1099,16 @@ class ErrorMsgMixin:
1054
1099
  PACKAGE_NAME = stack_var('__name__') or 'unspecified_package'
1055
1100
  PACKAGE_DOMAIN = 'org.test'
1056
1101
  PERMISSIONS = "INTERNET, VIBRATE, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE"
1057
- if os.path.exists(BUILD_CONFIG_FILE): # pragma: no cover
1102
+ if os_path_isfile(BUILD_CONFIG_FILE): # pragma: no cover
1058
1103
  PACKAGE_NAME, PACKAGE_DOMAIN, PERMISSIONS = build_config_variable_values(
1059
1104
  ('package.name', PACKAGE_NAME),
1060
1105
  ('package.domain', PACKAGE_DOMAIN),
1061
1106
  ('android.permissions', PERMISSIONS))
1062
1107
  elif os_platform == 'android': # pragma: no cover
1063
1108
  _importing_package = norm_path(stack_var('__file__') or 'empty_package' + PY_EXT)
1064
- if os.path.basename(_importing_package) in (PY_INIT, PY_MAIN):
1065
- _importing_package = os.path.dirname(_importing_package)
1066
- _importing_package = os.path.splitext(os.path.basename(_importing_package))[0]
1109
+ if os_path_basename(_importing_package) in (PY_INIT, PY_MAIN):
1110
+ _importing_package = os_path_dirname(_importing_package)
1111
+ _importing_package = os_path_splitext(os_path_basename(_importing_package))[0]
1067
1112
  write_file(f'{_importing_package}_debug.log', f"{BUILD_CONFIG_FILE} not bundled - using defaults\n", extra_mode='a')
1068
1113
 
1069
1114
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ae_base
3
- Version: 0.3.40
3
+ Version: 0.3.42
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
@@ -32,6 +32,7 @@ Requires-Dist: mypy; extra == "dev"
32
32
  Requires-Dist: pylint; extra == "dev"
33
33
  Requires-Dist: pytest; extra == "dev"
34
34
  Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: pytest-django; extra == "dev"
35
36
  Requires-Dist: typing; extra == "dev"
36
37
  Requires-Dist: types-setuptools; extra == "dev"
37
38
  Requires-Dist: wheel; extra == "dev"
@@ -46,6 +47,7 @@ Requires-Dist: mypy; extra == "tests"
46
47
  Requires-Dist: pylint; extra == "tests"
47
48
  Requires-Dist: pytest; extra == "tests"
48
49
  Requires-Dist: pytest-cov; extra == "tests"
50
+ Requires-Dist: pytest-django; extra == "tests"
49
51
  Requires-Dist: typing; extra == "tests"
50
52
  Requires-Dist: types-setuptools; extra == "tests"
51
53
  Requires-Dist: wheel; extra == "tests"
@@ -53,17 +55,17 @@ Requires-Dist: twine; extra == "tests"
53
55
 
54
56
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
55
57
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
56
- # base 0.3.40
58
+ # base 0.3.42
57
59
 
58
60
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
59
61
  https://gitlab.com/ae-group/ae_base)
60
62
  [![LatestPyPIrelease](
61
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.39?logo=python)](
62
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.39)
63
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.41?logo=python)](
64
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.41)
63
65
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
64
66
  https://pypi.org/project/ae-base/#history)
65
67
 
66
- >ae_base module 0.3.40.
68
+ >ae_base module 0.3.42.
67
69
 
68
70
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
69
71
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -10,6 +10,7 @@ mypy
10
10
  pylint
11
11
  pytest
12
12
  pytest-cov
13
+ pytest-django
13
14
  typing
14
15
  types-setuptools
15
16
  wheel
@@ -26,6 +27,7 @@ mypy
26
27
  pylint
27
28
  pytest
28
29
  pytest-cov
30
+ pytest-django
29
31
  typing
30
32
  types-setuptools
31
33
  wheel
@@ -1,4 +1,4 @@
1
- # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_project V0.3.30
1
+ # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_project V0.3.31
2
2
  """ setup this project with setuptools and aedev.setup_project. """
3
3
  import pprint
4
4
  import sys
@@ -1,4 +1,5 @@
1
1
  """ ae.base unit tests """
2
+ import datetime
2
3
  import os
3
4
  import tempfile
4
5
 
@@ -16,11 +17,11 @@ from typing import cast
16
17
  from ae.base import (
17
18
  BUILD_CONFIG_FILE, DOTENV_FILE_NAME, PY_EXT, PY_INIT, PY_MAIN, TESTS_FOLDER, UNSET,
18
19
  app_name_guess, build_config_variable_values, camel_to_snake, deep_dict_update, dummy_function, duplicates, env_str,
19
- force_encoding, full_stack_trace, import_module, instantiate_config_parser, in_wd,
20
+ filename2uri, force_encoding, full_stack_trace, import_module, instantiate_config_parser, in_wd,
20
21
  load_env_var_defaults, load_dotenvs, main_file_paths_parts, module_attr, module_file_path, module_name,
21
22
  norm_line_sep, norm_name, norm_path, now_str, os_host_name, os_local_ip, _os_platform, os_user_name,
22
23
  parse_dotenv, project_main_file, read_file, round_traditional, snake_to_camel, stack_frames, stack_var, stack_vars,
23
- sys_env_dict, sys_env_text, to_ascii, filename2uri, uri2filename, write_file, ErrorMsgMixin)
24
+ sys_env_dict, sys_env_text, to_ascii, uri2filename, utc_datetime, write_file, ErrorMsgMixin)
24
25
 
25
26
 
26
27
  tst_uri1 = "schema://user:pwd@domain/path_root/path_sub\\path+file% Üml?ä|ït.path_ext*\"<>"
@@ -203,6 +204,10 @@ class TestBaseHelpers:
203
204
  os.environ['NON_ALPHA_NUM_CHARS_69'] = vv
204
205
  assert env_str(ev, convert_name=True) == vv
205
206
 
207
+ def test_filename2uri(self):
208
+ assert filename2uri(tst_fna1) == tst_uri1
209
+ assert uri2filename(filename2uri(tst_fna1)) == tst_fna1
210
+
206
211
  def test_force_encoding_bytes(self):
207
212
  s = 'äöü'
208
213
 
@@ -245,7 +250,7 @@ class TestBaseHelpers:
245
250
 
246
251
  def test_import_module_local_module(self):
247
252
  module = "mod_2_tst"
248
- mod_file = os.path.join(TESTS_FOLDER, module + PY_EXT)
253
+ mod_file = cast(str, os.path.join(TESTS_FOLDER, module + PY_EXT))
249
254
  cur_dir = os.getcwd()
250
255
  try:
251
256
  write_file(mod_file, "mod_var = 'mod_var_val'")
@@ -676,7 +681,8 @@ class TestBaseHelpers:
676
681
  assert sys_env_dict().get('bundle_dir') is None
677
682
  sys.frozen = True
678
683
  assert sys_env_dict().get('bundle_dir')
679
- del sys.__dict__['frozen']
684
+ # noinspection PyUnresolvedReferences
685
+ del sys.__dict__['frozen'] # sys.__dict__.pop('frozen')
680
686
  assert sys_env_dict().get('bundle_dir') is None
681
687
 
682
688
  def test_sys_env_text(self):
@@ -702,14 +708,15 @@ class TestBaseHelpers:
702
708
  assert to_ascii('ß') == 'ss'
703
709
  assert to_ascii('€') == 'Euro'
704
710
 
705
- def test_filename2uri(self):
706
- assert filename2uri(tst_fna1) == tst_uri1
707
- assert uri2filename(filename2uri(tst_fna1)) == tst_fna1
708
-
709
711
  def test_uri2filename(self):
710
712
  assert uri2filename(tst_uri1) == tst_fna1
711
713
  assert filename2uri(uri2filename(tst_uri1)) == tst_uri1
712
714
 
715
+ def test_utc_datetime(self):
716
+ dt1 = utc_datetime()
717
+ dt2 = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
718
+ assert dt2 - dt1 < datetime.timedelta(seconds=1)
719
+
713
720
  def test_uri_file_name(self):
714
721
  try:
715
722
  write_file(tst_fna1, "tst uri file content")
@@ -739,6 +746,11 @@ class TestBaseHelpers:
739
746
  assert os.path.exists(test_file)
740
747
  assert os.path.isfile(test_file)
741
748
  assert read_file(test_file, extra_mode="b") == content
749
+
750
+ write_file(test_file, content) # 'b' in extra_mode arg is optional because content is bytes array
751
+ assert os.path.exists(test_file)
752
+ assert os.path.isfile(test_file)
753
+ assert read_file(test_file, extra_mode="b") == content
742
754
  finally:
743
755
  if os.path.exists(test_file):
744
756
  os.remove(test_file)
@@ -749,7 +761,7 @@ class TestModuleHelpers:
749
761
  namespace = TESTS_FOLDER
750
762
  mod_name = 'test_module_name'
751
763
  att_name = 'test_module_func'
752
- module_file = os.path.join(namespace, mod_name + PY_EXT)
764
+ module_file = cast(str, os.path.join(namespace, mod_name + PY_EXT))
753
765
  try:
754
766
  write_file(module_file, f"def {att_name}(*args, **kwargs):\n return args, kwargs\n")
755
767
  args = (1, '2')
@@ -777,7 +789,7 @@ class TestModuleHelpers:
777
789
  namespace = TESTS_FOLDER
778
790
  mod_name = 'test_module_name'
779
791
  att_name = 'test_module_func'
780
- module_file = os.path.join(namespace, mod_name + PY_EXT)
792
+ module_file = cast(str, os.path.join(namespace, mod_name + PY_EXT))
781
793
  try:
782
794
  write_file(module_file, f"def {att_name}(arg1, args2, kwarg1='default'):\n return arg1, arg2, kwarg1\n")
783
795
 
@@ -802,7 +814,7 @@ class TestModuleHelpers:
802
814
  def test_module_attr_module_ref(self):
803
815
  namespace = TESTS_FOLDER
804
816
  mod_name = 'test_module_name'
805
- module_file = os.path.join(namespace, mod_name + PY_EXT)
817
+ module_file = cast(str, os.path.join(namespace, mod_name + PY_EXT))
806
818
  cur_dir = os.getcwd()
807
819
  try:
808
820
  write_file(module_file, "# empty module")
@@ -825,7 +837,7 @@ class TestModuleHelpers:
825
837
  namespace = TESTS_FOLDER
826
838
  mod_name = 'test_module_name'
827
839
  att_name = 'test_module_func'
828
- module_file = os.path.join(namespace, mod_name + PY_EXT)
840
+ module_file = cast(str, os.path.join(namespace, mod_name + PY_EXT))
829
841
  cur_dir = os.getcwd()
830
842
  try:
831
843
  write_file(module_file, f"""def {att_name}(*args, **kwargs):\n pass\n""")
File without changes