file-groups 0.3.0__tar.gz → 0.3.1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. {file_groups-0.3.0 → file_groups-0.3.1}/.copier-answers.yml +2 -1
  2. {file_groups-0.3.0 → file_groups-0.3.1}/.gitignore +4 -2
  3. {file_groups-0.3.0 → file_groups-0.3.1}/PKG-INFO +4 -3
  4. {file_groups-0.3.0 → file_groups-0.3.1}/file_groups.egg-info/PKG-INFO +5 -4
  5. {file_groups-0.3.0 → file_groups-0.3.1}/file_groups.egg-info/SOURCES.txt +1 -0
  6. {file_groups-0.3.0 → file_groups-0.3.1}/noxfile.py +16 -5
  7. {file_groups-0.3.0 → file_groups-0.3.1}/pytest.ini +1 -1
  8. {file_groups-0.3.0 → file_groups-0.3.1}/setup.cfg +3 -2
  9. {file_groups-0.3.0 → file_groups-0.3.1}/src/__init__.py +2 -0
  10. {file_groups-0.3.0 → file_groups-0.3.1}/src/config_files.py +107 -82
  11. {file_groups-0.3.0 → file_groups-0.3.1}/src/groups.py +43 -39
  12. {file_groups-0.3.0 → file_groups-0.3.1}/src/handler.py +1 -1
  13. {file_groups-0.3.0 → file_groups-0.3.1}/src/handler_compare.py +1 -1
  14. {file_groups-0.3.0 → file_groups-0.3.1}/test/.coveragerc +3 -1
  15. {file_groups-0.3.0 → file_groups-0.3.1}/test/config_files_test.py +90 -131
  16. {file_groups-0.3.0 → file_groups-0.3.1}/test/conftest.py +50 -21
  17. file_groups-0.3.1/test/groups/config_files_test.py +33 -0
  18. {file_groups-0.3.0 → file_groups-0.3.1}/test/groups/directory_symlinks_test.py +4 -4
  19. {file_groups-0.3.0 → file_groups-0.3.1}/test/groups/re_protect_test.py +15 -0
  20. file_groups-0.3.1/test/in/configs/file_groups_group_dirs_by_config_file_protect/home/file_groups.conf +11 -0
  21. {file_groups-0.3.0 → file_groups-0.3.1}/test/version_test.py +1 -1
  22. file_groups-0.3.0/test/groups/config_files_test.py +0 -54
  23. {file_groups-0.3.0 → file_groups-0.3.1}/.pylintrc +0 -0
  24. {file_groups-0.3.0 → file_groups-0.3.1}/LICENSE.txt +0 -0
  25. {file_groups-0.3.0 → file_groups-0.3.1}/README.rst +0 -0
  26. {file_groups-0.3.0 → file_groups-0.3.1}/file_groups.egg-info/dependency_links.txt +0 -0
  27. {file_groups-0.3.0 → file_groups-0.3.1}/file_groups.egg-info/requires.txt +0 -0
  28. {file_groups-0.3.0 → file_groups-0.3.1}/file_groups.egg-info/top_level.txt +0 -0
  29. {file_groups-0.3.0 → file_groups-0.3.1}/file_groups.egg-info/zip-safe +0 -0
  30. {file_groups-0.3.0 → file_groups-0.3.1}/pyproject.toml +0 -0
  31. {file_groups-0.3.0 → file_groups-0.3.1}/setup.py +0 -0
  32. {file_groups-0.3.0 → file_groups-0.3.1}/src/compare_files.py +0 -0
  33. {file_groups-0.3.0 → file_groups-0.3.1}/src/py.typed +0 -0
  34. {file_groups-0.3.0 → file_groups-0.3.1}/src/types.py +0 -0
  35. {file_groups-0.3.0 → file_groups-0.3.1}/test/__init__.py +0 -0
  36. {file_groups-0.3.0 → file_groups-0.3.1}/test/compare_files_test.py +0 -0
  37. {file_groups-0.3.0 → file_groups-0.3.1}/test/groups/__init__.py +0 -0
  38. {file_groups-0.3.0 → file_groups-0.3.1}/test/groups/core_test.py +0 -0
  39. {file_groups-0.3.0 → file_groups-0.3.1}/test/groups/utils.py +0 -0
  40. {file_groups-0.3.0 → file_groups-0.3.1}/test/handler/__init__.py +0 -0
  41. {file_groups-0.3.0 → file_groups-0.3.1}/test/handler/core_test.py +0 -0
  42. {file_groups-0.3.0 → file_groups-0.3.1}/test/handler/private_test.py +0 -0
  43. {file_groups-0.3.0 → file_groups-0.3.1}/test/handler/regex_protection_test.py +0 -0
  44. {file_groups-0.3.0 → file_groups-0.3.1}/test/handler/utils.py +0 -0
  45. {file_groups-0.3.0 → file_groups-0.3.1}/test/handler_compare_test.py +0 -0
  46. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_inherit_global_recursive/home/file_groups.conf +0 -0
  47. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_inherit_global_recursive/sys/file_groups.conf +0 -0
  48. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_specified/direct.conf +0 -0
  49. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_sys_config_file_no_global/sys/file_groups.conf +0 -0
  50. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_sys_user_config_files_additional_appdirs/home/an_app.conf +0 -0
  51. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_sys_user_config_files_additional_appdirs/home/file_groups.conf +0 -0
  52. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_sys_user_config_files_additional_appdirs/sys/file_groups.conf +0 -0
  53. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_sys_user_config_files_no_global/home/file_groups.conf +0 -0
  54. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_sys_user_config_files_no_global/sys/file_groups.conf +0 -0
  55. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_two_in_same_config_dir/home/.file_groups.conf +0 -0
  56. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_two_in_same_config_dir/home/file_groups.conf +0 -0
  57. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_unknown_protect_sub_key_config_dir/sys/file_groups.conf +0 -0
  58. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/config_files_user_config_file_no_global/home/file_groups.conf +0 -0
  59. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/file_groups_group_files_by_config_protect/home/file_groups.conf +0 -0
  60. {file_groups-0.3.0 → file_groups-0.3.1}/test/in/configs/file_groups_group_files_by_config_protect/sys/file_groups.conf +0 -0
  61. {file_groups-0.3.0 → file_groups-0.3.1}/test/mypy_requirements.txt +0 -0
  62. {file_groups-0.3.0 → file_groups-0.3.1}/test/perf/protect_home_work_on_pictures.py +0 -0
  63. {file_groups-0.3.0 → file_groups-0.3.1}/test/perf/requirements.txt +0 -0
  64. {file_groups-0.3.0 → file_groups-0.3.1}/test/pylint_requirements.txt +0 -0
  65. {file_groups-0.3.0 → file_groups-0.3.1}/test/requirements.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # Changes here will be overwritten by Copier
2
- _commit: bbb7699
2
+ _commit: 1ddef8a
3
3
  _src_path: git@github.com:lhupfeldt/copier_python_package_template.git
4
4
  author_email: lhn@hupfeldtit.dk
5
5
  author_name: Lars Hupfeldt Nielsen
@@ -11,5 +11,6 @@ description: Group files into 'protect' and 'work_on' and provide operations for
11
11
  git_repo_url: https://github.com/lhupfeldt/file_groups.git
12
12
  github_organization: lhupfeldt
13
13
  legal_entity: ApS
14
+ local_devel_dependency: ''
14
15
  module_name: file_groups
15
16
  package_name: file_groups
@@ -1,5 +1,5 @@
1
1
  *~
2
- *.pyc
2
+ *.py[cod]
3
3
  __pycache__
4
4
  .cache
5
5
  .coverage
@@ -13,8 +13,10 @@ dist
13
13
  .tox
14
14
  .nox
15
15
  .mypy_cache
16
+ *_flymake.py
17
+ flycheck_*.py
16
18
  build
17
19
  test/out
18
20
 
19
21
  # Sphinx
20
- doc/_build/*
22
+ docs/_build/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: file_groups
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Group files into 'protect' and 'work_on' and provide operations for safe delete/move and symlink handling.
5
5
  Home-page: https://github.com/lhupfeldt/file_groups.git
6
6
  Author: Lars Hupfeldt Nielsen
@@ -11,9 +11,10 @@ Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: BSD License
12
12
  Classifier: Natural Language :: English
13
13
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.13
16
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.10
17
18
  Classifier: Topic :: Software Development :: Libraries
18
19
  Requires-Python: >=3.10
19
20
  Description-Content-Type: text/x-rst
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: file-groups
3
- Version: 0.3.0
2
+ Name: file_groups
3
+ Version: 0.3.1
4
4
  Summary: Group files into 'protect' and 'work_on' and provide operations for safe delete/move and symlink handling.
5
5
  Home-page: https://github.com/lhupfeldt/file_groups.git
6
6
  Author: Lars Hupfeldt Nielsen
@@ -11,9 +11,10 @@ Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: BSD License
12
12
  Classifier: Natural Language :: English
13
13
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.13
16
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.10
17
18
  Classifier: Topic :: Software Development :: Libraries
18
19
  Requires-Python: >=3.10
19
20
  Description-Content-Type: text/x-rst
@@ -62,6 +62,7 @@ test/in/configs/config_files_two_in_same_config_dir/home/.file_groups.conf
62
62
  test/in/configs/config_files_two_in_same_config_dir/home/file_groups.conf
63
63
  test/in/configs/config_files_unknown_protect_sub_key_config_dir/sys/file_groups.conf
64
64
  test/in/configs/config_files_user_config_file_no_global/home/file_groups.conf
65
+ test/in/configs/file_groups_group_dirs_by_config_file_protect/home/file_groups.conf
65
66
  test/in/configs/file_groups_group_files_by_config_protect/home/file_groups.conf
66
67
  test/in/configs/file_groups_group_files_by_config_protect/sys/file_groups.conf
67
68
  test/perf/protect_home_work_on_pictures.py
@@ -2,14 +2,16 @@
2
2
 
3
3
  # Use nox >= 2023.4.22
4
4
 
5
+ import os
5
6
  from pathlib import Path
7
+ import shutil
6
8
 
7
9
  import nox
8
10
 
9
11
 
10
12
  _HERE = Path(__file__).absolute().parent
11
13
  _TEST_DIR = _HERE/"test"
12
- _PY_VERSIONS = ["3.12", "3.11", "3.10"]
14
+ _PY_VERSIONS = ['3.13', '3.12', '3.11', '3.10']
13
15
 
14
16
  nox.options.error_on_missing_interpreters = True
15
17
 
@@ -17,17 +19,17 @@ nox.options.error_on_missing_interpreters = True
17
19
  @nox.session(python=_PY_VERSIONS, reuse_venv=True)
18
20
  def typecheck(session):
19
21
  session.install("-e", ".", "mypy>=1.5.1")
20
- session.run("mypy", "-v", str(_HERE/"src"))
22
+ session.run("mypy", str(_HERE/"src"))
21
23
 
22
24
 
23
- # TODO: pylint-pytest does not support 3.12
24
- @nox.session(python="3.11", reuse_venv=True)
25
+ @nox.session(python=_PY_VERSIONS[0], reuse_venv=True)
25
26
  def pylint(session):
26
- session.install(".", "pylint>=2.16.1,<3.0.0", "pylint-pytest>=1.1.2")
27
+ session.install(".", "pylint>=3.3.1", "pylint-pytest>=1.1.8")
27
28
 
28
29
  print("\nPylint src")
29
30
  disable_checks = "missing-module-docstring"
30
31
  session.run("pylint", "--fail-under", "10", "--disable", disable_checks, str(_HERE/"src"))
32
+
31
33
  print("\nPylint test sources")
32
34
  disable_checks += ",missing-class-docstring,missing-function-docstring,multiple-imports,invalid-name,duplicate-code"
33
35
  session.run("pylint", "--fail-under", "9.94", "--variable-rgx", r"[a-z_][a-z0-9_]{1,30}$", "--disable", disable_checks, str(_HERE/"test"))
@@ -37,3 +39,12 @@ def pylint(session):
37
39
  def unit(session):
38
40
  session.install(".", "pytest>=7.4.1", "coverage>=7.3.1", "pytest-cov>=4.1.0")
39
41
  session.run("pytest", "--import-mode=append", "--cov", "--cov-report=term-missing", f"--cov-config={_TEST_DIR}/.coveragerc", *session.posargs)
42
+
43
+
44
+ @nox.session(python=_PY_VERSIONS[0], reuse_venv=True)
45
+ def build(session):
46
+ if Path("dist").is_dir():
47
+ shutil.rmtree("dist")
48
+ session.install("build>=1.0.3", "twine>=4.0.2")
49
+ session.run("python", "-m", "build")
50
+ session.run("python", "-m", "twine", "check", "dist/*")
@@ -3,4 +3,4 @@
3
3
  [pytest]
4
4
  minversion = 7.4.1
5
5
  testpaths = test
6
- norecursedirs = __pycache__ utils perf
6
+ norecursedirs = __pycache__ utils perf docs dist
@@ -13,9 +13,10 @@ classifiers =
13
13
  License :: OSI Approved :: BSD License
14
14
  Natural Language :: English
15
15
  Operating System :: OS Independent
16
- Programming Language :: Python :: 3.10
17
- Programming Language :: Python :: 3.11
16
+ Programming Language :: Python :: 3.13
18
17
  Programming Language :: Python :: 3.12
18
+ Programming Language :: Python :: 3.11
19
+ Programming Language :: Python :: 3.10
19
20
  Topic :: Software Development :: Libraries
20
21
 
21
22
  [options]
@@ -1,3 +1,5 @@
1
+ """Set __version__ property."""
2
+
1
3
  from importlib.metadata import version # type: ignore
2
4
 
3
5
 
@@ -7,7 +7,7 @@ import itertools
7
7
  from pprint import pformat
8
8
  import logging
9
9
  from dataclasses import dataclass
10
- from typing import Tuple, Sequence, cast
10
+ from typing import Tuple, Sequence, Any
11
11
 
12
12
  from appdirs import AppDirs # type: ignore
13
13
 
@@ -22,22 +22,47 @@ class ConfigException(Exception):
22
22
 
23
23
 
24
24
  @dataclass
25
- class DirConfig():
26
- """Hold protect config for a directory, or global (site or user) config."""
27
- protect: dict[str, set[re.Pattern]]
25
+ class ProtectConfig():
26
+ """Hold global (site or user) protect config."""
27
+ protect_recursive: set[re.Pattern]
28
+
29
+ def __json__(self):
30
+ return {
31
+ ProtectConfig.__name__: {
32
+ "protect_recursive": [str(pat) for pat in self.protect_recursive],
33
+ }
34
+ }
35
+
36
+
37
+ @dataclass
38
+ class DirConfig(ProtectConfig):
39
+ """Hold directory specific protect config."""
40
+ protect_local: set[re.Pattern]
28
41
  config_dir: Path|None
29
- config_files: Sequence[str]
42
+ config_files: list[str]
30
43
 
31
44
  def is_protected(self, ff: FsPath):
32
45
  """If ff id protected by a regex pattern then return the pattern, otherwise return None."""
33
46
 
34
- for pattern in itertools.chain(self.protect["local"], self.protect["recursive"]):
47
+ # _LOG.debug("ff '%s'", ff)
48
+ for pattern in itertools.chain(self.protect_local, self.protect_recursive):
35
49
  if os.sep in str(pattern):
36
- # Match against full path
50
+ # _LOG.debug("Pattern '%s' has path sep", pattern)
37
51
  assert os.path.isabs(ff), f"Expected absolute path, got '{ff}'"
52
+
53
+ # Search against full path
38
54
  if pattern.search(os.fspath(ff)):
39
55
  return pattern
40
56
 
57
+ # Attempt exact match against path relative to , i.e. if pattern starts with '^'.
58
+ # This makes sense for patterns specified on commandline
59
+ cwd = os.getcwd()
60
+ ff_relative = str(Path(ff).relative_to(cwd))
61
+ # _LOG.debug("ff '%s' relative to start dir'%s'", ff_relative, cwd)
62
+
63
+ if pattern.match(ff_relative):
64
+ return pattern
65
+
41
66
  elif pattern.search(ff.name):
42
67
  return pattern
43
68
 
@@ -45,8 +70,8 @@ class DirConfig():
45
70
 
46
71
  def __json__(self):
47
72
  return {
48
- "DirConfig": {
49
- "protect": {key: list(str(pat) for pat in val) for key, val in self.protect.items()},
73
+ DirConfig.__name__: super().__json__()[ProtectConfig.__name__] | {
74
+ "protect_local": [str(pat) for pat in self.protect_local],
50
75
  "config_dir": str(self.config_dir),
51
76
  "config_files": self.config_files,
52
77
  }
@@ -113,15 +138,16 @@ class ConfigFiles():
113
138
  protect: An optional sequence of regexes to be added to protect[recursive] for all directories.
114
139
  ignore_config_dirs_config_files: Ignore config files in standard config directories.
115
140
  ignore_per_directory_config_files: Ignore config files in collected directories.
116
- remember_configs: Store loaded and merged configs in `dir_configs` member variable.
141
+ remember_configs: Store loaded and merged configs in `per_dir_configs` member variable.
117
142
  app_dirs: Provide your own instance of AppDirs in addition to or as a replacement of the default to add config file names and path.
118
143
  Configuration from later entries have higher precedence.
119
144
  Note that if no AppDirs are specified, no config files will be loaded, neither from config dirs, nor from collected directories.
120
145
  See: https://pypi.org/project/appdirs/
121
146
 
122
147
  Members:
123
- remember_configs: Whether per directory resolved/merged configs are stored in `dir_configs`.
124
- dir_configs: dict[str: dict] Mapping from dir name to directory specific config dict. Only if remember_configs is True.
148
+ conf_file_names: File names which are config files.
149
+ remember_configs: Whether per directory resolved/merged configs are stored in `per_dir_configs`.
150
+ per_dir_configs: Mapping from dir name to directory specific config dict. Only if remember_configs is True.
125
151
  """
126
152
 
127
153
  default_appdirs: AppDirs = AppDirs("file_groups", "Hupfeldt_IT")
@@ -131,7 +157,7 @@ class ConfigFiles():
131
157
  _valid_dir_protect_scopes = ("local", "recursive")
132
158
  _valid_config_dir_protect_scopes = ("local", "recursive", "global")
133
159
 
134
- def __init__(
160
+ def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments
135
161
  self, protect: Sequence[re.Pattern] = (),
136
162
  ignore_config_dirs_config_files=False, ignore_per_directory_config_files=False, remember_configs=True,
137
163
  app_dirs: Sequence[AppDirs]|None = None,
@@ -140,38 +166,34 @@ class ConfigFiles():
140
166
  ):
141
167
  super().__init__()
142
168
 
143
- self._global_config = DirConfig({
144
- "local": set(),
145
- "recursive": set(protect),
146
- }, None, ())
147
-
169
+ self._global_config = ProtectConfig(set(protect))
148
170
  self.remember_configs = remember_configs
149
- self.per_dir_configs: dict[str, DirConfig] = {} # key is abs_dir_path
171
+ self.per_dir_configs: dict[Path, DirConfig] = {} # key is abs_dir_path
150
172
  self.ignore_per_directory_config_files = ignore_per_directory_config_files
151
173
 
152
174
  app_dirs = app_dirs or (ConfigFiles.default_appdirs,)
153
- self.conf_file_names = tuple((apd.appname + ".conf", "." + apd.appname + ".conf") for apd in app_dirs)
154
- _LOG.debug("Conf file names: %s", self.conf_file_names)
175
+ self.conf_file_name_pairs = tuple((apd.appname + ".conf", "." + apd.appname + ".conf") for apd in app_dirs)
176
+ _LOG.debug("Conf file names: %s", self.conf_file_name_pairs)
177
+ self.conf_file_names = list(itertools.chain.from_iterable(self.conf_file_name_pairs))
178
+
179
+ self.ignore_config_dirs_config_files = ignore_config_dirs_config_files
155
180
  self.config_dirs = []
156
- if not ignore_config_dirs_config_files:
157
- for appd in app_dirs:
158
- self.config_dirs.extend(appd.site_config_dir.split(':'))
159
- for appd in app_dirs:
160
- self.config_dirs.append(appd.user_config_dir)
181
+ for appd in app_dirs:
182
+ self.config_dirs.extend(appd.site_config_dir.split(':'))
183
+ for appd in app_dirs:
184
+ self.config_dirs.append(appd.user_config_dir)
161
185
 
162
186
  self.config_file = config_file
163
187
 
164
188
  # self.default_config_file_example = self.default_config_file.with_suffix('.example.py')
165
189
 
166
- def _read_and_validate_config_file_for_one_appname( # pylint: disable=too-many-locals
167
- self, conf_dir: Path, conf_file_name_pair: Sequence[str], parent_conf: DirConfig, valid_protect_scopes: Tuple[str, ...], ignore_config_files: bool
168
- ) -> Tuple[dict[str, set[re.Pattern]], str|None]:
169
- """Read config file, validate keys and compile regexes and merge with parent.
190
+ def _read_and_eval_config_file_for_one_appname(
191
+ self, conf_dir: Path, conf_file_name_pair: Sequence[str], ignore_config_files: bool
192
+ ) -> Tuple[dict[str, Any], Path|None]:
193
+ """Read config file.
170
194
 
171
195
  Error if config files are found both with and withput '.' prefix.
172
- Merge parent conf into conf_dir conf (if any) and return the merged dict. The parent conf is not modified.
173
-
174
- Return: merged config dict with compiled regexes, config file name. If no config files is found, then return inherited parent conf and None.
196
+ Return: Config dict, config file name, or if no config files is found, None, None.
175
197
  """
176
198
 
177
199
  assert conf_dir.is_absolute()
@@ -180,27 +202,41 @@ class ConfigFiles():
180
202
  match [conf_dir/cfn for cfn in conf_file_name_pair if (conf_dir/cfn).exists()]:
181
203
  case []:
182
204
  _LOG.debug("No config file in directory %s", conf_dir)
183
- no_conf_file: dict[str, set[re.Pattern]] = {
184
- "local": set(),
185
- "recursive": parent_conf.protect["recursive"]
186
- }
187
- return no_conf_file, None
205
+ return {}, None
188
206
 
189
207
  case [conf_file]:
190
208
  if ignore_config_files:
191
209
  _LOG.debug("Ignoring config file: %s", conf_file)
192
- return self._global_config.protect, None
210
+ return {}, None
193
211
 
194
212
  _LOG.debug("Read config file: %s", conf_file)
195
213
  with open(conf_file, encoding="utf-8") as fh:
196
214
  new_config = ast.literal_eval(fh.read())
197
215
  _LOG.debug("%s", pformat(new_config))
216
+ return new_config, conf_file
198
217
 
199
218
  case config_files:
200
219
  msg = f"More than one config file in dir '{conf_dir}': {[cf.name for cf in config_files]}."
201
220
  _LOG.debug("%s", msg)
202
221
  raise ConfigException(msg)
203
222
 
223
+ def _read_and_validate_config_file_for_one_appname(
224
+ self, conf_dir: Path, conf_file_name_pair: Sequence[str], valid_protect_scopes: Tuple[str, ...], ignore_config_files: bool
225
+ ) -> Tuple[dict[str, set[re.Pattern]], Path|None]:
226
+ """Read config file, validate keys and compile regexes.
227
+
228
+ Error if config files are found both with and withput '.' prefix.
229
+
230
+ Return: merged config dict with compiled regexes, config file name. If no config files is found, then return empty sets and None.
231
+ """
232
+
233
+ new_config, conf_file = self._read_and_eval_config_file_for_one_appname(conf_dir, conf_file_name_pair, ignore_config_files)
234
+ if not new_config:
235
+ return {
236
+ "local": set(),
237
+ "recursive": set(),
238
+ }, None
239
+
204
240
  try:
205
241
  protect_conf: dict[str, set[re.Pattern]] = new_config[self._fg_key][self._protect_key]
206
242
  except KeyError as ex:
@@ -213,8 +249,6 @@ class ConfigFiles():
213
249
  raise ConfigException(msg)
214
250
 
215
251
  protect_conf[key] = set(re.compile(pattern) for pattern in val)
216
- if key == "recursive":
217
- protect_conf[key].update(parent_conf.protect[key])
218
252
 
219
253
  for key in self._valid_dir_protect_scopes: # Do NOT use the 'valid_protect_scopes' argument here
220
254
  protect_conf.setdefault(key, set())
@@ -223,55 +257,34 @@ class ConfigFiles():
223
257
  if _LOG.isEnabledFor(lvl):
224
258
  _LOG.log(lvl, "Merged directory config:\n%s", pformat(new_config))
225
259
 
226
- return protect_conf, conf_file.name
227
-
228
- def _read_and_validate_config_files(
229
- self, conf_dir: Path, parent_conf: DirConfig, valid_protect_scopes: Tuple[str, ...], ignore_config_files: bool) -> DirConfig:
230
- cfg_merge: dict[str, set[re.Pattern]] = {}
231
- cfg_files: list[str] = []
232
- for conf_file_name_pair in self.conf_file_names:
233
- cfg, cfg_file = self._read_and_validate_config_file_for_one_appname(
234
- conf_dir, conf_file_name_pair, parent_conf, valid_protect_scopes, ignore_config_files)
235
- for key, val in cfg.items():
236
- cfg_merge.setdefault(key, set()).update(val)
237
- if cfg_file:
238
- cfg_files.append(cfg_file)
239
-
240
- return DirConfig(cfg_merge, conf_dir, cfg_files)
260
+ return protect_conf, conf_file
241
261
 
242
262
  def load_config_dir_files(self) -> None:
243
263
  """Load config files from platform standard directories and specified config file, if any."""
244
264
 
245
- def merge_one_config_to_global(conf_dir, new_config):
246
- if self.remember_configs:
247
- self.per_dir_configs[str(conf_dir)] = new_config
265
+ if not self.ignore_config_dirs_config_files:
266
+ _LOG.debug("config_dirs: %s", self.config_dirs)
267
+ for conf_dir in self.config_dirs:
268
+ conf_dir = Path(conf_dir)
269
+ if not conf_dir.exists():
270
+ continue
248
271
 
249
- self._global_config.protect["recursive"].update(new_config.protect.get("global", set()))
250
- _LOG.debug("Merged global config:\n %s", new_config)
251
-
252
- try:
253
- del new_config.protect['global']
254
- except KeyError:
255
- pass
256
-
257
- _LOG.debug("config_dirs: %s", self.config_dirs)
258
- for conf_dir in self.config_dirs:
259
- conf_dir = Path(conf_dir)
260
- if not conf_dir.exists():
261
- continue
262
-
263
- new_config = self._read_and_validate_config_files(conf_dir, self._global_config, self._valid_config_dir_protect_scopes, False)
264
- merge_one_config_to_global(conf_dir, new_config)
272
+ for conf_file_name_pair in self.conf_file_name_pairs:
273
+ cfg, _ = self._read_and_validate_config_file_for_one_appname(
274
+ conf_dir, conf_file_name_pair, self._valid_config_dir_protect_scopes, self.ignore_config_dirs_config_files)
275
+ self._global_config.protect_recursive |= cfg.get("global", set())
265
276
 
266
277
  if self.config_file:
278
+ _LOG.debug("specified config_file: %s", self.config_file)
267
279
  conf_dir = self.config_file.parent.absolute()
268
280
  conf_name = self.config_file.name
269
- cfg, filename = self._read_and_validate_config_file_for_one_appname(
270
- conf_dir, (conf_name,), self._global_config, self._valid_config_dir_protect_scopes, ignore_config_files=False)
271
- if not filename:
281
+ cfg, fpath = self._read_and_validate_config_file_for_one_appname(
282
+ conf_dir, (conf_name,), self._valid_config_dir_protect_scopes, self.ignore_config_dirs_config_files)
283
+ if not fpath:
272
284
  raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self.config_file))
285
+ self._global_config.protect_recursive |= cfg.get("global", set())
273
286
 
274
- merge_one_config_to_global(conf_dir, DirConfig(cfg, conf_dir, (cast(str, filename),)))
287
+ _LOG.debug("Merged global config:\n %s", self._global_config)
275
288
 
276
289
  def dir_config(self, conf_dir: Path, parent_conf: DirConfig|None) -> DirConfig:
277
290
  """Read and merge config file from directory 'conf_dir' with 'parent_conf'.
@@ -279,12 +292,24 @@ class ConfigFiles():
279
292
  If directory has no parent in the file_groups included dirs, then None should be supplied as parent_conf.
280
293
  """
281
294
 
282
- new_config = self._read_and_validate_config_files(
283
- conf_dir, parent_conf or self._global_config, self._valid_dir_protect_scopes, self.ignore_per_directory_config_files)
295
+ cfg_merge_local: set[re.Pattern] = set()
296
+ cfg_merge_recursive: set[re.Pattern] = set()
297
+ cfg_files: list[str] = []
298
+
299
+ for conf_file_name_pair in self.conf_file_name_pairs:
300
+ cfg, cfg_file = self._read_and_validate_config_file_for_one_appname(
301
+ conf_dir, conf_file_name_pair, self._valid_dir_protect_scopes, self.ignore_per_directory_config_files)
302
+ cfg_merge_local.update(cfg.get("local", set()))
303
+ cfg_merge_recursive.update(cfg.get("recursive", set()))
304
+ if cfg_file:
305
+ cfg_files.append(cfg_file.name)
306
+
307
+ parent_protect_recursive = parent_conf.protect_recursive if parent_conf else self._global_config.protect_recursive
308
+ new_config = DirConfig(cfg_merge_recursive | parent_protect_recursive, cfg_merge_local, conf_dir, cfg_files)
284
309
  _LOG.debug("new_config:\n %s", new_config)
285
310
 
286
311
  if self.remember_configs:
287
- self.per_dir_configs[str(conf_dir)] = new_config
312
+ self.per_dir_configs[conf_dir] = new_config
288
313
  # _LOG.debug("per_dir_configs:\n %s", self.per_dir_configs)
289
314
 
290
315
  return new_config
@@ -7,7 +7,7 @@ from dataclasses import dataclass
7
7
  from itertools import chain
8
8
  from enum import Enum
9
9
  import logging
10
- from typing import Sequence
10
+ from typing import Sequence, cast
11
11
 
12
12
  from .config_files import DirConfig, ConfigFiles
13
13
 
@@ -89,7 +89,7 @@ class FileGroups():
89
89
  Note: Since these files are excluded from protection, it means they er NOT protected!
90
90
  work_include: ONLY include files matching regex in the may_work_on files (does not apply to symlinks). Default: Include ALL.
91
91
 
92
- config_files: Load config files. See config_files.ConfigFiles.
92
+ config_files: Load config files. See config_files.ConfigFiles. Note that the default 'None' means use the `config_files.ConfigFiles` class with default arguments.
93
93
  """
94
94
 
95
95
  def __init__(
@@ -169,8 +169,46 @@ class FileGroups():
169
169
 
170
170
  checked_dirs: set[str] = set()
171
171
 
172
+ def handle_entry(abs_dir_path: str, group: _Group, other_group: _Group, dir_config: DirConfig, entry: DirEntry):
173
+ """Put entry in the correct group. Call 'find_group' if entry is a directory."""
174
+ if group.typ is GroupType.MAY_WORK_ON:
175
+ # Check for match against configured protect patterns, if match, then the file must got to protect group instead
176
+ pattern = dir_config.is_protected(entry)
177
+ if pattern:
178
+ _LOG.debug("find %s - '%s' is protected by regex %s, assigning to group %s instead.", group.typ.name, entry.path, pattern, other_group.typ.name)
179
+ group, other_group = other_group, group
180
+
181
+ if entry.is_dir(follow_symlinks=False):
182
+ if entry.path in other_group.dirs:
183
+ _LOG.debug("find %s - '%s' is in '%s' dir list and not in '%s' dir list", group.typ.name, entry.path, other_group.typ.name, group.typ.name)
184
+ find_group(entry.path, other_group, group, dir_config)
185
+ return
186
+
187
+ find_group(entry.path, group, other_group, dir_config)
188
+ return
189
+
190
+ if entry.name in self.config_files.conf_file_names:
191
+ return
192
+
193
+ if entry.is_symlink():
194
+ # cast: https://github.com/python/mypy/issues/11964
195
+ points_to = os.readlink(cast(str, entry))
196
+ abs_points_to = os.path.normpath(os.path.join(abs_dir_path, points_to))
197
+
198
+ if entry.is_dir(follow_symlinks=True):
199
+ _LOG.debug("find %s - '%s' -> '%s' is a symlink to a directory - ignoring", group.typ.name, entry.path, points_to)
200
+ group.num_directory_symlinks += 1
201
+ return
202
+
203
+ group.symlinks[entry.path] = entry
204
+ group.symlinks_by_abs_points_to[abs_points_to].append(entry)
205
+ return
206
+
207
+ _LOG.debug("find %s - entry name: %s", group.typ.name, entry.name)
208
+ group.add_entry_match(entry)
209
+
172
210
  def find_group(abs_dir_path: str, group: _Group, other_group: _Group, parent_conf: DirConfig|None):
173
- """Find all files belonging to 'group'"""
211
+ """Recursively find all files belonging to 'group'"""
174
212
  _LOG.debug("find %s: %s", group.typ.name, abs_dir_path)
175
213
  if abs_dir_path in checked_dirs:
176
214
  _LOG.debug("directory already checked")
@@ -180,48 +218,14 @@ class FileGroups():
180
218
  dir_config = self.config_files.dir_config(Path(abs_dir_path), parent_conf)
181
219
 
182
220
  for entry in os.scandir(abs_dir_path):
183
- if entry.is_dir(follow_symlinks=False):
184
- if entry.path in other_group.dirs:
185
- _LOG.debug("find %s - '%s' is in '%s' dir list and not in '%s' dir list", group.typ.name, entry.path, other_group.typ.name, group.typ.name)
186
- find_group(entry.path, other_group, group, dir_config)
187
- continue
188
-
189
- find_group(entry.path, group, other_group, dir_config)
190
- continue
191
-
192
- if entry.name in dir_config.config_files:
193
- continue
194
-
195
- current_group = group
196
- if group.typ is GroupType.MAY_WORK_ON:
197
- # We need to check for match against configured protect patterns, if match, then the file must got to protect group instead
198
- pattern = dir_config.is_protected(entry)
199
- if pattern:
200
- _LOG.debug("find %s - '%s' is protected by regex %s, assigning to group %s instead.", group.typ.name, entry.path, pattern, other_group.typ.name)
201
- current_group = other_group
202
-
203
- if entry.is_symlink():
204
- points_to = os.readlink(entry)
205
- abs_points_to = os.path.normpath(os.path.join(abs_dir_path, points_to))
206
-
207
- if entry.is_dir(follow_symlinks=True):
208
- _LOG.debug("find %s - '%s' -> '%s' is a symlink to a directory - ignoring", current_group.typ.name, entry.path, points_to)
209
- current_group.num_directory_symlinks += 1
210
- continue
211
-
212
- current_group.symlinks[entry.path] = entry
213
- current_group.symlinks_by_abs_points_to[abs_points_to].append(entry)
214
- continue
215
-
216
- _LOG.debug("find %s - entry name: %s", current_group.typ.name, entry.name)
217
- current_group.add_entry_match(entry)
221
+ handle_entry(abs_dir_path, group, other_group, dir_config, entry)
218
222
 
219
223
  checked_dirs.add(abs_dir_path)
220
224
 
221
225
  for any_dir in sorted(chain(self.must_protect.dirs, self.may_work_on.dirs), key=lambda dd: len(Path(dd).parts)):
222
226
  parent_dir = Path(any_dir)
223
227
  while len(parent_dir.parts) > 1:
224
- parent_conf = self.config_files.per_dir_configs.get(any_dir)
228
+ parent_conf = self.config_files.per_dir_configs.get(parent_dir)
225
229
  if parent_conf:
226
230
  break
227
231
 
@@ -28,7 +28,7 @@ class FileHandler(FileGroups):
28
28
  they could have logically been made to point to a file in a protect dir.
29
29
  """
30
30
 
31
- def __init__(
31
+ def __init__( # pylint: disable=too-many-arguments
32
32
  self,
33
33
  protect_dirs_seq: Sequence[Path], work_dirs_seq: Sequence[Path],
34
34
  *,
@@ -22,7 +22,7 @@ class FileHandlerCompare(FileHandler):
22
22
  fcmp: Object providing compare function.
23
23
  """
24
24
 
25
- def __init__(
25
+ def __init__( # pylint: disable=too-many-arguments
26
26
  self,
27
27
  protect_dirs_seq: Sequence[Path], work_dirs_seq: Sequence[Path], fcmp: CompareFiles,
28
28
  *,
@@ -24,6 +24,8 @@ partial_branches =
24
24
  pragma: no branch
25
25
 
26
26
  omit =
27
+ .nox/*
27
28
  test/*
28
29
  experiments
29
- .nox/*
30
+ *_flymake.py
31
+ flycheck_*.py