rolfedh-doc-utils 0.1.15__tar.gz → 0.1.17__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.
Files changed (50) hide show
  1. {rolfedh_doc_utils-0.1.15/rolfedh_doc_utils.egg-info → rolfedh_doc_utils-0.1.17}/PKG-INFO +19 -3
  2. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/README.md +18 -2
  3. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/archive_unused_files.py +3 -0
  4. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/archive_unused_images.py +3 -0
  5. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/check_scannability.py +3 -0
  6. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/unused_attributes.py +50 -0
  7. rolfedh_doc_utils-0.1.17/doc_utils/version_check.py +204 -0
  8. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/extract_link_attributes.py +3 -0
  9. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/find_unused_attributes.py +15 -1
  10. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/format_asciidoc_spacing.py +3 -0
  11. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/pyproject.toml +1 -1
  12. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/replace_link_attributes.py +3 -0
  13. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17/rolfedh_doc_utils.egg-info}/PKG-INFO +19 -3
  14. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/rolfedh_doc_utils.egg-info/SOURCES.txt +1 -0
  15. rolfedh_doc_utils-0.1.17/tests/test_unused_attributes.py +199 -0
  16. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/validate_links.py +3 -0
  17. rolfedh_doc_utils-0.1.15/tests/test_unused_attributes.py +0 -44
  18. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/LICENSE +0 -0
  19. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/__init__.py +0 -0
  20. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/extract_link_attributes.py +0 -0
  21. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/file_utils.py +0 -0
  22. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/format_asciidoc_spacing.py +0 -0
  23. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/replace_link_attributes.py +0 -0
  24. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/scannability.py +0 -0
  25. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/spinner.py +0 -0
  26. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/topic_map_parser.py +0 -0
  27. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/unused_adoc.py +0 -0
  28. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/unused_images.py +0 -0
  29. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/doc_utils/validate_links.py +0 -0
  30. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
  31. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/rolfedh_doc_utils.egg-info/entry_points.txt +0 -0
  32. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
  33. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/rolfedh_doc_utils.egg-info/top_level.txt +0 -0
  34. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/setup.cfg +0 -0
  35. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/setup.py +0 -0
  36. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_archive_unused_files.py +0 -0
  37. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_archive_unused_images.py +0 -0
  38. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_auto_discovery.py +0 -0
  39. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_check_scannability.py +0 -0
  40. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_cli_entry_points.py +0 -0
  41. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_extract_link_attributes.py +0 -0
  42. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_file_utils.py +0 -0
  43. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_fixture_archive_unused_files.py +0 -0
  44. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_fixture_archive_unused_images.py +0 -0
  45. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_fixture_check_scannability.py +0 -0
  46. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_parse_exclude_list.py +0 -0
  47. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_replace_link_attributes.py +0 -0
  48. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_symlink_handling.py +0 -0
  49. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_topic_map_parser.py +0 -0
  50. {rolfedh_doc_utils-0.1.15 → rolfedh_doc_utils-0.1.17}/tests/test_validate_links.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -190,11 +190,27 @@ Before submitting PRs:
190
190
 
191
191
  ## 📊 Project Status
192
192
 
193
- - **Latest Version**: 0.1.6
193
+ - **Latest Version**: 0.1.16 (with automatic update notifications)
194
194
  - **Python Support**: 3.8+
195
- - **Test Coverage**: 66+ tests (100% passing)
195
+ - **Test Coverage**: 112+ tests (100% passing)
196
196
  - **Dependencies**: Minimal (PyYAML for OpenShift-docs support)
197
197
 
198
+ ### 🔔 Update Notifications
199
+
200
+ All tools now check for updates automatically and notify you when a new version is available:
201
+
202
+ ```
203
+ 📦 Update available: 0.1.15 → 0.1.16
204
+ Run: pip install --upgrade rolfedh-doc-utils
205
+ ```
206
+
207
+ To disable update checks, set the environment variable:
208
+ ```bash
209
+ export DOC_UTILS_NO_VERSION_CHECK=1
210
+ ```
211
+
212
+ Update checks are cached for 24 hours to minimize network requests.
213
+
198
214
  ## 🔗 Resources
199
215
 
200
216
  - [Documentation](https://rolfedh.github.io/doc-utils/)
@@ -157,11 +157,27 @@ Before submitting PRs:
157
157
 
158
158
  ## 📊 Project Status
159
159
 
160
- - **Latest Version**: 0.1.6
160
+ - **Latest Version**: 0.1.16 (with automatic update notifications)
161
161
  - **Python Support**: 3.8+
162
- - **Test Coverage**: 66+ tests (100% passing)
162
+ - **Test Coverage**: 112+ tests (100% passing)
163
163
  - **Dependencies**: Minimal (PyYAML for OpenShift-docs support)
164
164
 
165
+ ### 🔔 Update Notifications
166
+
167
+ All tools now check for updates automatically and notify you when a new version is available:
168
+
169
+ ```
170
+ 📦 Update available: 0.1.15 → 0.1.16
171
+ Run: pip install --upgrade rolfedh-doc-utils
172
+ ```
173
+
174
+ To disable update checks, set the environment variable:
175
+ ```bash
176
+ export DOC_UTILS_NO_VERSION_CHECK=1
177
+ ```
178
+
179
+ Update checks are cached for 24 hours to minimize network requests.
180
+
165
181
  ## 🔗 Resources
166
182
 
167
183
  - [Documentation](https://rolfedh.github.io/doc-utils/)
@@ -9,10 +9,13 @@ For full documentation and usage examples, see archive_unused_files.md in this d
9
9
 
10
10
  import argparse
11
11
  from doc_utils.unused_adoc import find_unused_adoc
12
+ from doc_utils.version_check import check_version_on_startup
12
13
  from doc_utils.file_utils import parse_exclude_list_file
13
14
 
14
15
  from doc_utils.spinner import Spinner
15
16
  def main():
17
+ # Check for updates (non-blocking, won't interfere with tool operation)
18
+ check_version_on_startup()
16
19
  parser = argparse.ArgumentParser(
17
20
  description='Archive unused AsciiDoc files.',
18
21
  epilog='By default, automatically discovers all modules and assemblies directories in the repository.'
@@ -8,10 +8,13 @@ For full documentation and usage examples, see archive_unused_files.md in this d
8
8
 
9
9
  import argparse
10
10
  from doc_utils.unused_images import find_unused_images
11
+ from doc_utils.version_check import check_version_on_startup
11
12
  from doc_utils.file_utils import parse_exclude_list_file
12
13
 
13
14
  from doc_utils.spinner import Spinner
14
15
  def main():
16
+ # Check for updates (non-blocking, won't interfere with tool operation)
17
+ check_version_on_startup()
15
18
  parser = argparse.ArgumentParser(description='Archive unused image files.')
16
19
  parser.add_argument('--archive', action='store_true', help='Move the files to a dated zip in the archive directory.')
17
20
  parser.add_argument('--exclude-dir', action='append', default=[], help='Directory to exclude (can be used multiple times).')
@@ -17,6 +17,7 @@ import sys
17
17
  import argparse
18
18
  from datetime import datetime
19
19
  from doc_utils.scannability import check_scannability
20
+ from doc_utils.version_check import check_version_on_startup
20
21
  from doc_utils.file_utils import collect_files, parse_exclude_list_file
21
22
 
22
23
  from doc_utils.spinner import Spinner
@@ -27,6 +28,8 @@ def print_help():
27
28
  print(__doc__)
28
29
 
29
30
  def main():
31
+ # Check for updates (non-blocking, won't interfere with tool operation)
32
+ check_version_on_startup()
30
33
  # Manual check for -h or --help to display the full docstring
31
34
  if '-h' in sys.argv or '--help' in sys.argv:
32
35
  print_help()
@@ -50,13 +50,22 @@ def find_adoc_files(root_dir: str) -> List[str]:
50
50
 
51
51
  def scan_for_attribute_usage(adoc_files: List[str], attributes: Set[str]) -> Set[str]:
52
52
  used = set()
53
+ # Pattern for attribute references: {attribute-name}
53
54
  attr_pattern = re.compile(r'\{([\w-]+)\}')
55
+ # Patterns for conditional directives: ifdef::attr[], ifndef::attr[], endif::attr[]
56
+ conditional_pattern = re.compile(r'(?:ifdef|ifndef|endif)::([\w-]+)\[')
57
+
54
58
  for file in adoc_files:
55
59
  with open(file, 'r', encoding='utf-8') as f:
56
60
  for line in f:
61
+ # Check for {attribute} references
57
62
  for match in attr_pattern.findall(line):
58
63
  if match in attributes:
59
64
  used.add(match)
65
+ # Check for ifdef::attribute[], ifndef::attribute[], endif::attribute[]
66
+ for match in conditional_pattern.findall(line):
67
+ if match in attributes:
68
+ used.add(match)
60
69
  return used
61
70
 
62
71
  def find_attributes_files(root_dir: str = '.') -> List[str]:
@@ -136,3 +145,44 @@ def find_unused_attributes(attr_file: str, adoc_root: str = '.') -> List[str]:
136
145
  used = scan_for_attribute_usage(adoc_files, attributes)
137
146
  unused = sorted(attributes - used)
138
147
  return unused
148
+
149
+
150
+ def comment_out_unused_attributes(attr_file: str, unused_attrs: List[str]) -> int:
151
+ """
152
+ Comment out unused attributes in the attributes file.
153
+
154
+ Args:
155
+ attr_file: Path to the attributes file
156
+ unused_attrs: List of unused attribute names
157
+
158
+ Returns:
159
+ Number of attributes commented out
160
+ """
161
+ if not unused_attrs:
162
+ return 0
163
+
164
+ # Read the file
165
+ with open(attr_file, 'r', encoding='utf-8') as f:
166
+ lines = f.readlines()
167
+
168
+ # Create a set for faster lookup
169
+ unused_set = set(unused_attrs)
170
+ commented_count = 0
171
+
172
+ # Process each line
173
+ new_lines = []
174
+ for line in lines:
175
+ # Check if this line defines an attribute
176
+ match = re.match(r'^:([\w-]+):', line)
177
+ if match and match.group(1) in unused_set:
178
+ # Comment out this line
179
+ new_lines.append(f'// Unused {line}')
180
+ commented_count += 1
181
+ else:
182
+ new_lines.append(line)
183
+
184
+ # Write back to the file
185
+ with open(attr_file, 'w', encoding='utf-8') as f:
186
+ f.writelines(new_lines)
187
+
188
+ return commented_count
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Version checking utility for doc-utils.
4
+
5
+ Checks PyPI for the latest version and notifies users if an update is available.
6
+ Includes caching to avoid excessive PyPI requests.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import time
13
+ import urllib.request
14
+ import urllib.error
15
+ from datetime import datetime, timedelta
16
+ from importlib.metadata import version as get_installed_version
17
+ from pathlib import Path
18
+ from typing import Optional, Tuple
19
+
20
+
21
+ def get_cache_dir() -> Path:
22
+ """Get the cache directory for version check data."""
23
+ # Use XDG_CACHE_HOME if set, otherwise ~/.cache
24
+ cache_home = os.environ.get('XDG_CACHE_HOME')
25
+ if cache_home:
26
+ cache_dir = Path(cache_home) / 'doc-utils'
27
+ else:
28
+ cache_dir = Path.home() / '.cache' / 'doc-utils'
29
+
30
+ cache_dir.mkdir(parents=True, exist_ok=True)
31
+ return cache_dir
32
+
33
+
34
+ def get_cache_file() -> Path:
35
+ """Get the cache file path."""
36
+ return get_cache_dir() / 'version_check.json'
37
+
38
+
39
+ def read_cache() -> Optional[dict]:
40
+ """Read version check cache."""
41
+ cache_file = get_cache_file()
42
+ if not cache_file.exists():
43
+ return None
44
+
45
+ try:
46
+ with open(cache_file, 'r') as f:
47
+ data = json.load(f)
48
+ # Check if cache is still valid (24 hours)
49
+ last_check = datetime.fromisoformat(data['last_check'])
50
+ if datetime.now() - last_check < timedelta(hours=24):
51
+ return data
52
+ except (json.JSONDecodeError, KeyError, ValueError):
53
+ pass
54
+
55
+ return None
56
+
57
+
58
+ def write_cache(latest_version: str, current_version: str):
59
+ """Write version check cache."""
60
+ cache_file = get_cache_file()
61
+ data = {
62
+ 'last_check': datetime.now().isoformat(),
63
+ 'latest_version': latest_version,
64
+ 'current_version': current_version,
65
+ }
66
+
67
+ try:
68
+ with open(cache_file, 'w') as f:
69
+ json.dump(data, f)
70
+ except Exception:
71
+ # Silently fail if we can't write cache
72
+ pass
73
+
74
+
75
+ def fetch_latest_version() -> Optional[str]:
76
+ """Fetch the latest version from PyPI."""
77
+ try:
78
+ # Use PyPI JSON API
79
+ url = 'https://pypi.org/pypi/rolfedh-doc-utils/json'
80
+ with urllib.request.urlopen(url, timeout=2) as response:
81
+ data = json.loads(response.read())
82
+ return data['info']['version']
83
+ except (urllib.error.URLError, json.JSONDecodeError, KeyError, TimeoutError):
84
+ # Silently fail if we can't reach PyPI
85
+ return None
86
+
87
+
88
+ def parse_version(version_str: str) -> Tuple[int, ...]:
89
+ """Parse version string into tuple of integers for comparison."""
90
+ try:
91
+ # Remove any pre-release or dev suffixes
92
+ version_str = version_str.split('+')[0].split('-')[0]
93
+ return tuple(int(x) for x in version_str.split('.'))
94
+ except (ValueError, AttributeError):
95
+ return (0,)
96
+
97
+
98
+ def check_for_update(force_check: bool = False) -> Optional[str]:
99
+ """
100
+ Check if a newer version is available.
101
+
102
+ Args:
103
+ force_check: If True, bypass cache and always check PyPI
104
+
105
+ Returns:
106
+ The latest version string if an update is available, None otherwise
107
+ """
108
+ try:
109
+ current_version = get_installed_version('rolfedh-doc-utils')
110
+ except Exception:
111
+ # Can't determine installed version
112
+ return None
113
+
114
+ # Check cache first (unless forced)
115
+ if not force_check:
116
+ cache_data = read_cache()
117
+ if cache_data:
118
+ latest_version = cache_data['latest_version']
119
+ # Only notify if there's a newer version
120
+ if parse_version(latest_version) > parse_version(current_version):
121
+ return latest_version
122
+ return None
123
+
124
+ # Fetch from PyPI
125
+ latest_version = fetch_latest_version()
126
+ if not latest_version:
127
+ return None
128
+
129
+ # Cache the result
130
+ write_cache(latest_version, current_version)
131
+
132
+ # Check if update is available
133
+ if parse_version(latest_version) > parse_version(current_version):
134
+ return latest_version
135
+
136
+ return None
137
+
138
+
139
+ def show_update_notification(latest_version: str, current_version: str = None):
140
+ """Show update notification to user."""
141
+ if not current_version:
142
+ try:
143
+ current_version = get_installed_version('rolfedh-doc-utils')
144
+ except Exception:
145
+ current_version = 'unknown'
146
+
147
+ # Use stderr to avoid interfering with tool output
148
+ print(f"\n📦 Update available: {current_version} → {latest_version}", file=sys.stderr)
149
+ print(f" Run: pip install --upgrade rolfedh-doc-utils", file=sys.stderr)
150
+ print("", file=sys.stderr)
151
+
152
+
153
+ def check_version_on_startup():
154
+ """
155
+ Check for updates on tool startup.
156
+
157
+ This should be called early in the main() function of each CLI tool.
158
+ It runs asynchronously and won't block the tool execution.
159
+ """
160
+ # Skip version check in certain conditions
161
+ if any([
162
+ os.environ.get('DOC_UTILS_NO_VERSION_CHECK'), # User opt-out
163
+ os.environ.get('CI'), # Running in CI
164
+ not sys.stderr.isatty(), # Not running in terminal
165
+ ]):
166
+ return
167
+
168
+ try:
169
+ latest_version = check_for_update()
170
+ if latest_version:
171
+ show_update_notification(latest_version)
172
+ except Exception:
173
+ # Never let version checking break the tool
174
+ pass
175
+
176
+
177
+ def disable_version_check():
178
+ """
179
+ Instructions for disabling version check.
180
+
181
+ Users can disable by setting DOC_UTILS_NO_VERSION_CHECK environment variable.
182
+ """
183
+ print("To disable version checking, set the environment variable:")
184
+ print(" export DOC_UTILS_NO_VERSION_CHECK=1")
185
+ print("\nOr add it to your shell configuration file.")
186
+
187
+
188
+ if __name__ == "__main__":
189
+ # For testing/debugging
190
+ import argparse
191
+ parser = argparse.ArgumentParser(description="Check for doc-utils updates")
192
+ parser.add_argument('--force', action='store_true', help='Force check (bypass cache)')
193
+ parser.add_argument('--disable-instructions', action='store_true',
194
+ help='Show instructions for disabling version check')
195
+ args = parser.parse_args()
196
+
197
+ if args.disable_instructions:
198
+ disable_version_check()
199
+ else:
200
+ latest = check_for_update(force_check=args.force)
201
+ if latest:
202
+ show_update_notification(latest)
203
+ else:
204
+ print("You are running the latest version!")
@@ -10,9 +10,12 @@ attribute references.
10
10
  import argparse
11
11
  import sys
12
12
  from doc_utils.extract_link_attributes import extract_link_attributes
13
+ from doc_utils.version_check import check_version_on_startup
13
14
 
14
15
 
15
16
  def main():
17
+ # Check for updates (non-blocking, won't interfere with tool operation)
18
+ check_version_on_startup()
16
19
  """Main entry point for the extract-link-attributes CLI tool."""
17
20
  parser = argparse.ArgumentParser(
18
21
  description='Extract link and xref macros containing attributes into attribute definitions',
@@ -12,10 +12,13 @@ import argparse
12
12
  import os
13
13
  import sys
14
14
  from datetime import datetime
15
- from doc_utils.unused_attributes import find_unused_attributes, find_attributes_files, select_attributes_file
15
+ from doc_utils.unused_attributes import find_unused_attributes, find_attributes_files, select_attributes_file, comment_out_unused_attributes
16
16
  from doc_utils.spinner import Spinner
17
+ from doc_utils.version_check import check_version_on_startup
17
18
 
18
19
  def main():
20
+ # Check for updates (non-blocking, won't interfere with tool operation)
21
+ check_version_on_startup()
19
22
  parser = argparse.ArgumentParser(description='Find unused AsciiDoc attributes.')
20
23
  parser.add_argument(
21
24
  'attributes_file',
@@ -23,6 +26,7 @@ def main():
23
26
  help='Path to the attributes file. If not specified, auto-discovers attributes files.'
24
27
  )
25
28
  parser.add_argument('-o', '--output', action='store_true', help='Write results to a timestamped txt file in your home directory.')
29
+ parser.add_argument('-c', '--comment-out', action='store_true', help='Comment out unused attributes in the attributes file with "// Unused".')
26
30
  args = parser.parse_args()
27
31
 
28
32
  # Determine which attributes file to use
@@ -81,6 +85,16 @@ def main():
81
85
  f.write(output + '\n')
82
86
  print(f'Results written to: {filename}')
83
87
 
88
+ if args.comment_out and output:
89
+ # Ask for confirmation before modifying the file
90
+ print(f'\nThis will comment out {len(unused)} unused attributes in: {attr_file}')
91
+ response = input('Continue? (y/n): ').strip().lower()
92
+ if response == 'y':
93
+ commented_count = comment_out_unused_attributes(attr_file, unused)
94
+ print(f'Commented out {commented_count} unused attributes in: {attr_file}')
95
+ else:
96
+ print('Operation cancelled.')
97
+
84
98
  return 0
85
99
 
86
100
  if __name__ == '__main__':
@@ -10,6 +10,7 @@ import sys
10
10
  from pathlib import Path
11
11
 
12
12
  from doc_utils.format_asciidoc_spacing import process_file, find_adoc_files
13
+ from doc_utils.version_check import check_version_on_startup
13
14
 
14
15
 
15
16
  from doc_utils.spinner import Spinner
@@ -27,6 +28,8 @@ def print_colored(message: str, color: str = Colors.NC) -> None:
27
28
 
28
29
 
29
30
  def main():
31
+ # Check for updates (non-blocking, won't interfere with tool operation)
32
+ check_version_on_startup()
30
33
  """Main entry point"""
31
34
  parser = argparse.ArgumentParser(
32
35
  description="Format AsciiDoc files to ensure proper spacing",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rolfedh-doc-utils"
7
- version = "0.1.15"
7
+ version = "0.1.17"
8
8
  description = "CLI tools for AsciiDoc documentation projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -19,6 +19,7 @@ from doc_utils.replace_link_attributes import (
19
19
  replace_link_attributes_in_file,
20
20
  find_adoc_files
21
21
  )
22
+ from doc_utils.version_check import check_version_on_startup
22
23
  from doc_utils.spinner import Spinner
23
24
 
24
25
 
@@ -83,6 +84,8 @@ def prompt_for_attributes_file(attributes_files: list[Path]) -> Optional[Path]:
83
84
 
84
85
 
85
86
  def main():
87
+ # Check for updates (non-blocking, won't interfere with tool operation)
88
+ check_version_on_startup()
86
89
  parser = argparse.ArgumentParser(
87
90
  description='Replace AsciiDoc attributes within link macros with their actual values.'
88
91
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -190,11 +190,27 @@ Before submitting PRs:
190
190
 
191
191
  ## 📊 Project Status
192
192
 
193
- - **Latest Version**: 0.1.6
193
+ - **Latest Version**: 0.1.16 (with automatic update notifications)
194
194
  - **Python Support**: 3.8+
195
- - **Test Coverage**: 66+ tests (100% passing)
195
+ - **Test Coverage**: 112+ tests (100% passing)
196
196
  - **Dependencies**: Minimal (PyYAML for OpenShift-docs support)
197
197
 
198
+ ### 🔔 Update Notifications
199
+
200
+ All tools now check for updates automatically and notify you when a new version is available:
201
+
202
+ ```
203
+ 📦 Update available: 0.1.15 → 0.1.16
204
+ Run: pip install --upgrade rolfedh-doc-utils
205
+ ```
206
+
207
+ To disable update checks, set the environment variable:
208
+ ```bash
209
+ export DOC_UTILS_NO_VERSION_CHECK=1
210
+ ```
211
+
212
+ Update checks are cached for 24 hours to minimize network requests.
213
+
198
214
  ## 🔗 Resources
199
215
 
200
216
  - [Documentation](https://rolfedh.github.io/doc-utils/)
@@ -22,6 +22,7 @@ doc_utils/unused_adoc.py
22
22
  doc_utils/unused_attributes.py
23
23
  doc_utils/unused_images.py
24
24
  doc_utils/validate_links.py
25
+ doc_utils/version_check.py
25
26
  rolfedh_doc_utils.egg-info/PKG-INFO
26
27
  rolfedh_doc_utils.egg-info/SOURCES.txt
27
28
  rolfedh_doc_utils.egg-info/dependency_links.txt
@@ -0,0 +1,199 @@
1
+ import os
2
+ import tempfile
3
+ from doc_utils.unused_attributes import parse_attributes_file, find_adoc_files, scan_for_attribute_usage, find_unused_attributes, comment_out_unused_attributes
4
+
5
+ def make_file(path, content):
6
+ with open(path, 'w', encoding='utf-8') as f:
7
+ f.write(content)
8
+
9
+ def test_parse_attributes_file():
10
+ with tempfile.TemporaryDirectory() as tmpdir:
11
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
12
+ make_file(attr_path, ":foo: bar\n:bar: baz\n:unused: 123\n")
13
+ attrs = parse_attributes_file(attr_path)
14
+ assert 'foo' in attrs
15
+ assert 'bar' in attrs
16
+ assert 'unused' in attrs
17
+
18
+ def test_find_adoc_files_and_usage():
19
+ with tempfile.TemporaryDirectory() as tmpdir:
20
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
21
+ make_file(attr_path, ":foo: bar\n:bar: baz\n:unused: 123\n")
22
+ adoc1 = os.path.join(tmpdir, 'a.adoc')
23
+ adoc2 = os.path.join(tmpdir, 'b.adoc')
24
+ make_file(adoc1, "This uses {foo} and {bar}.")
25
+ make_file(adoc2, "No attributes here.")
26
+ adoc_files = find_adoc_files(tmpdir)
27
+ attrs = parse_attributes_file(attr_path)
28
+ used = scan_for_attribute_usage(adoc_files, attrs)
29
+ assert 'foo' in used
30
+ assert 'bar' in used
31
+ assert 'unused' not in used
32
+
33
+ def test_find_unused_attributes():
34
+ with tempfile.TemporaryDirectory() as tmpdir:
35
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
36
+ make_file(attr_path, ":foo: bar\n:bar: baz\n:unused: 123\n")
37
+ adoc1 = os.path.join(tmpdir, 'a.adoc')
38
+ adoc2 = os.path.join(tmpdir, 'b.adoc')
39
+ make_file(adoc1, "This uses {foo} and {bar}.")
40
+ make_file(adoc2, "No attributes here.")
41
+ unused = find_unused_attributes(attr_path, tmpdir)
42
+ assert 'unused' in unused
43
+ assert 'foo' not in unused
44
+ assert 'bar' not in unused
45
+
46
+ def test_conditional_directives_ifdef():
47
+ """Test that ifdef::attribute:: is recognized as usage."""
48
+ with tempfile.TemporaryDirectory() as tmpdir:
49
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
50
+ make_file(attr_path, ":rh-only:\n:downstream:\n:unused:\n")
51
+ adoc1 = os.path.join(tmpdir, 'test.adoc')
52
+ make_file(adoc1, "ifdef::rh-only[]\nSome content\nendif::rh-only[]\n")
53
+ attrs = parse_attributes_file(attr_path)
54
+ adoc_files = find_adoc_files(tmpdir)
55
+ used = scan_for_attribute_usage(adoc_files, attrs)
56
+ assert 'rh-only' in used
57
+ assert 'downstream' not in used
58
+ assert 'unused' not in used
59
+
60
+ def test_conditional_directives_ifndef():
61
+ """Test that ifndef::attribute:: is recognized as usage."""
62
+ with tempfile.TemporaryDirectory() as tmpdir:
63
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
64
+ make_file(attr_path, ":no-feature:\n:unused:\n")
65
+ adoc1 = os.path.join(tmpdir, 'test.adoc')
66
+ make_file(adoc1, "ifndef::no-feature[]\nContent when feature exists\nendif::no-feature[]\n")
67
+ attrs = parse_attributes_file(attr_path)
68
+ adoc_files = find_adoc_files(tmpdir)
69
+ used = scan_for_attribute_usage(adoc_files, attrs)
70
+ assert 'no-feature' in used
71
+ assert 'unused' not in used
72
+
73
+ def test_conditional_directives_endif():
74
+ """Test that endif::attribute:: is recognized as usage."""
75
+ with tempfile.TemporaryDirectory() as tmpdir:
76
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
77
+ make_file(attr_path, ":my-attr:\n:unused:\n")
78
+ adoc1 = os.path.join(tmpdir, 'test.adoc')
79
+ make_file(adoc1, "ifdef::my-attr[]\nSome content\nendif::my-attr[]\n")
80
+ attrs = parse_attributes_file(attr_path)
81
+ adoc_files = find_adoc_files(tmpdir)
82
+ used = scan_for_attribute_usage(adoc_files, attrs)
83
+ assert 'my-attr' in used
84
+ assert 'unused' not in used
85
+
86
+ def test_mixed_usage_patterns():
87
+ """Test attributes used in both {attr} and ifdef::attr:: forms."""
88
+ with tempfile.TemporaryDirectory() as tmpdir:
89
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
90
+ make_file(attr_path, ":version: 1.0\n:rh-only:\n:unused:\n")
91
+ adoc1 = os.path.join(tmpdir, 'test.adoc')
92
+ make_file(adoc1, "Version {version}\nifdef::rh-only[]\nRH content\nendif::rh-only[]\n")
93
+ unused = find_unused_attributes(attr_path, tmpdir)
94
+ assert 'unused' in unused
95
+ assert 'version' not in unused
96
+ assert 'rh-only' not in unused
97
+
98
+ def test_no_prefix_attributes():
99
+ """Test attributes with 'no-' prefix used in ifndef directives."""
100
+ with tempfile.TemporaryDirectory() as tmpdir:
101
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
102
+ make_file(attr_path, ":no-quarkus-security-jpa-reactive:\n:no-webauthn-authentication:\n:unused:\n")
103
+ adoc1 = os.path.join(tmpdir, 'test.adoc')
104
+ make_file(adoc1, """
105
+ ifndef::no-quarkus-security-jpa-reactive[]
106
+ Content about JPA reactive security
107
+ endif::no-quarkus-security-jpa-reactive[]
108
+
109
+ ifndef::no-webauthn-authentication[]
110
+ Content about WebAuthn
111
+ endif::no-webauthn-authentication[]
112
+ """)
113
+ unused = find_unused_attributes(attr_path, tmpdir)
114
+ assert 'unused' in unused
115
+ assert 'no-quarkus-security-jpa-reactive' not in unused
116
+ assert 'no-webauthn-authentication' not in unused
117
+
118
+ def test_comment_out_unused_attributes():
119
+ """Test commenting out unused attributes in the attributes file."""
120
+ with tempfile.TemporaryDirectory() as tmpdir:
121
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
122
+ original_content = """:version: 1.0
123
+ :product: MyApp
124
+ :unused1: value1
125
+ :unused2: value2
126
+ :rh-only:
127
+ """
128
+ make_file(attr_path, original_content)
129
+
130
+ adoc1 = os.path.join(tmpdir, 'test.adoc')
131
+ make_file(adoc1, "Version {version} Product {product}\nifdef::rh-only[]\nContent\nendif::rh-only[]\n")
132
+
133
+ # Find unused attributes
134
+ unused = find_unused_attributes(attr_path, tmpdir)
135
+ assert 'unused1' in unused
136
+ assert 'unused2' in unused
137
+ assert 'version' not in unused
138
+ assert 'product' not in unused
139
+ assert 'rh-only' not in unused
140
+
141
+ # Comment out unused attributes
142
+ count = comment_out_unused_attributes(attr_path, unused)
143
+ assert count == 2
144
+
145
+ # Verify the file was modified correctly
146
+ with open(attr_path, 'r') as f:
147
+ content = f.read()
148
+
149
+ assert '// Unused :unused1: value1\n' in content
150
+ assert '// Unused :unused2: value2\n' in content
151
+ assert ':version: 1.0\n' in content
152
+ assert ':product: MyApp\n' in content
153
+ assert ':rh-only:\n' in content
154
+
155
+ def test_comment_out_empty_list():
156
+ """Test that comment_out_unused_attributes handles empty list correctly."""
157
+ with tempfile.TemporaryDirectory() as tmpdir:
158
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
159
+ original_content = ":version: 1.0\n:product: MyApp\n"
160
+ make_file(attr_path, original_content)
161
+
162
+ # Comment out with empty list
163
+ count = comment_out_unused_attributes(attr_path, [])
164
+ assert count == 0
165
+
166
+ # Verify file was not modified
167
+ with open(attr_path, 'r') as f:
168
+ content = f.read()
169
+
170
+ assert content == original_content
171
+
172
+ def test_comment_out_preserves_formatting():
173
+ """Test that commenting out preserves line formatting and comments."""
174
+ with tempfile.TemporaryDirectory() as tmpdir:
175
+ attr_path = os.path.join(tmpdir, 'attributes.adoc')
176
+ original_content = """// This is a comment
177
+ :version: 1.0
178
+
179
+ :unused: value with spaces
180
+ // Another comment
181
+ :product: MyApp
182
+ """
183
+ make_file(attr_path, original_content)
184
+
185
+ adoc1 = os.path.join(tmpdir, 'test.adoc')
186
+ make_file(adoc1, "Version {version} Product {product}\n")
187
+
188
+ unused = find_unused_attributes(attr_path, tmpdir)
189
+ count = comment_out_unused_attributes(attr_path, unused)
190
+ assert count == 1
191
+
192
+ with open(attr_path, 'r') as f:
193
+ content = f.read()
194
+
195
+ assert '// This is a comment\n' in content
196
+ assert '// Unused :unused: value with spaces\n' in content
197
+ assert '// Another comment\n' in content
198
+ assert ':version: 1.0\n' in content
199
+ assert ':product: MyApp\n' in content
@@ -12,10 +12,13 @@ import argparse
12
12
  import sys
13
13
  import json
14
14
  from doc_utils.validate_links import LinkValidator, parse_transpositions, format_results
15
+ from doc_utils.version_check import check_version_on_startup
15
16
  from doc_utils.spinner import Spinner
16
17
 
17
18
 
18
19
  def main():
20
+ # Check for updates (non-blocking, won't interfere with tool operation)
21
+ check_version_on_startup()
19
22
  """Main entry point for the validate-links CLI tool."""
20
23
  parser = argparse.ArgumentParser(
21
24
  description='Validate links in AsciiDoc documentation',
@@ -1,44 +0,0 @@
1
- import os
2
- import tempfile
3
- from doc_utils.unused_attributes import parse_attributes_file, find_adoc_files, scan_for_attribute_usage, find_unused_attributes
4
-
5
- def make_file(path, content):
6
- with open(path, 'w', encoding='utf-8') as f:
7
- f.write(content)
8
-
9
- def test_parse_attributes_file():
10
- with tempfile.TemporaryDirectory() as tmpdir:
11
- attr_path = os.path.join(tmpdir, 'attributes.adoc')
12
- make_file(attr_path, ":foo: bar\n:bar: baz\n:unused: 123\n")
13
- attrs = parse_attributes_file(attr_path)
14
- assert 'foo' in attrs
15
- assert 'bar' in attrs
16
- assert 'unused' in attrs
17
-
18
- def test_find_adoc_files_and_usage():
19
- with tempfile.TemporaryDirectory() as tmpdir:
20
- attr_path = os.path.join(tmpdir, 'attributes.adoc')
21
- make_file(attr_path, ":foo: bar\n:bar: baz\n:unused: 123\n")
22
- adoc1 = os.path.join(tmpdir, 'a.adoc')
23
- adoc2 = os.path.join(tmpdir, 'b.adoc')
24
- make_file(adoc1, "This uses {foo} and {bar}.")
25
- make_file(adoc2, "No attributes here.")
26
- adoc_files = find_adoc_files(tmpdir)
27
- attrs = parse_attributes_file(attr_path)
28
- used = scan_for_attribute_usage(adoc_files, attrs)
29
- assert 'foo' in used
30
- assert 'bar' in used
31
- assert 'unused' not in used
32
-
33
- def test_find_unused_attributes():
34
- with tempfile.TemporaryDirectory() as tmpdir:
35
- attr_path = os.path.join(tmpdir, 'attributes.adoc')
36
- make_file(attr_path, ":foo: bar\n:bar: baz\n:unused: 123\n")
37
- adoc1 = os.path.join(tmpdir, 'a.adoc')
38
- adoc2 = os.path.join(tmpdir, 'b.adoc')
39
- make_file(adoc1, "This uses {foo} and {bar}.")
40
- make_file(adoc2, "No attributes here.")
41
- unused = find_unused_attributes(attr_path, tmpdir)
42
- assert 'unused' in unused
43
- assert 'foo' not in unused
44
- assert 'bar' not in unused