mkdocs-simple-plugin 3.0.0__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: mkdocs-simple-plugin
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Plugin for adding simple wiki site creation from markdown files interspersed within your code with MkDocs.
5
5
  Project-URL: Documentation, http://www.althack.dev/mkdocs-simple-plugin
6
6
  Project-URL: Homepage, http://www.althack.dev/mkdocs-simple-plugin
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: Information Technology
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.7
19
18
  Classifier: Programming Language :: Python :: 3.8
20
19
  Classifier: Programming Language :: Python :: 3.9
21
20
  Classifier: Programming Language :: Python :: 3.10
@@ -23,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.11
23
22
  Requires-Python: >=3
24
23
  Requires-Dist: click>=7.1
25
24
  Requires-Dist: markupsafe>=2.1.1
26
- Requires-Dist: mkdocs>=1.4.0
25
+ Requires-Dist: mkdocs>=1.6.0
27
26
  Requires-Dist: pyyaml>=6.0
28
27
  Description-Content-Type: text/markdown
29
28
 
@@ -0,0 +1,55 @@
1
+ # Package Guide
2
+
3
+ ## Prerequisites
4
+
5
+ {% include "setup.snippet" %}
6
+
7
+ ## Building
8
+
9
+ {% include "build.snippet" %}
10
+
11
+ ## Testing
12
+
13
+ {% include "tests/linters.snippet" %}
14
+
15
+ {% include "tests/unit_tests.snippet" %}
16
+
17
+ {% include "tests/integration_tests.snippet" %}
18
+
19
+ {% include "tests/local_tests.snippet" %}
20
+
21
+ ## VSCode
22
+
23
+ This package includes a preconfigured Visual Studio Code (VSCode) workspace and development container, making it easier to get started with developing your plugin.
24
+
25
+ To get started with developing your plugin in VSCode, follow these steps:
26
+
27
+ 1. **Open the project in VSCode** Open VSCode and select the "Open Folder" option from the File menu. Navigate to the location where you've saved the project and select the root folder of the project.
28
+
29
+ 2. **Connect to the development container** VSCode will automatically detect the presence of the development container and prompt you to connect to it. Follow the on-screen instructions to connect to the container.
30
+
31
+ 3. **Run the build task** To build the plugin, you can use the preconfigured build task in VSCode. This task is defined in the `build.sh` file and can be run by using the "Run Build Task" option from the Terminal menu or by using the Ctrl + Shift + B shortcut.
32
+
33
+ 4. **Debug the plugin** You can use the VSCode debugger to inspect the code and debug your plugin. The debugger is configured in the launch.json file and can be started by using the "Start Debugging" option from the Debug menu or by using the F5 key.
34
+
35
+ For more information on how to use VSCode and Docker for development, please refer to [how I develop with VSCode and Docker](https://allisonthackston.com/articles/docker-development.html) and [how I use VSCode tasks](https://allisonthackston.com/articles/vscode-tasks.html).
36
+
37
+ ## Packaging
38
+
39
+ The project uses Hatch to build and package the plugin [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
40
+
41
+ Hatch is a Python packaging tool that helps simplify the process of building and distributing Python packages. It automates many manual steps, such as creating a setup script, creating a distribution package, and uploading the package to a repository, allowing developers to focus on writing code. Hatch is flexible and customizable, with options for specifying dependencies, including additional files, and setting up test suites, and is actively maintained with a growing community of users and contributors.
42
+
43
+ ### Build the package
44
+
45
+ ```bash
46
+ hatch build
47
+ ```
48
+
49
+ ### Publish the package
50
+
51
+ ```bash
52
+ hatch publish
53
+ ```
54
+
55
+ Please note that you may need to set up the appropriate credentials for the repository before you can publish the package. If you encounter any issues with publishing the package, please refer to the [Hatch documentation](https://hatch.pypa.io/latest/) for guidance.
@@ -68,7 +68,6 @@ def default_config():
68
68
  # Set the config variables via environment if exist
69
69
  maybe_set_string("site_name")
70
70
  maybe_set_string("site_url")
71
- maybe_set_string("site_dir")
72
71
  maybe_set_string("repo_url")
73
72
  maybe_set_dict("theme", "name")
74
73
  return config
@@ -86,7 +85,7 @@ def write_config(config_file, config):
86
85
  """Write configuration file."""
87
86
  if os.path.dirname(config_file):
88
87
  os.makedirs(os.path.dirname(config_file), exist_ok=True)
89
- with open(config_file, 'w+') as file:
88
+ with open(config_file, 'w+', encoding="utf-8") as file:
90
89
  try:
91
90
  yaml.dump(
92
91
  data=config,
@@ -106,7 +105,7 @@ def setup_config(config_file="mkdocs.yml"):
106
105
  # from the folder name.
107
106
  write_config(config_file, config)
108
107
  # Open the config file to verify settings.
109
- with open(config_file, 'r') as stream:
108
+ with open(config_file, 'r', encoding="utf-8") as stream:
110
109
  try:
111
110
  local_config = yaml.load(stream, yaml.Loader)
112
111
  if local_config:
@@ -90,14 +90,16 @@ mkdocs serve
90
90
  import os
91
91
  import tempfile
92
92
  import time
93
- import yaml
94
-
93
+ from typing import Callable, Literal
95
94
 
96
- from mkdocs.structure.files import Files, File
97
- from mkdocs.plugins import BasePlugin
98
- from mkdocs.config import config_options
95
+ import yaml
99
96
  from mkdocs import config as mkdocs_config
100
97
  from mkdocs import utils
98
+ from mkdocs.config import config_options
99
+ from mkdocs.config.defaults import MkDocsConfig
100
+ from mkdocs.livereload import LiveReloadServer
101
+ from mkdocs.plugins import BasePlugin
102
+ from mkdocs.structure.files import File, Files
101
103
 
102
104
  from mkdocs_simple_plugin.simple import Simple
103
105
 
@@ -107,22 +109,48 @@ class SimplePlugin(BasePlugin):
107
109
 
108
110
  # md file=config_scheme.snippet
109
111
  config_scheme = (
110
- # ### include_folders
112
+ # ### include_folders (renamed)
113
+ #
114
+ # Renamed [folders](#folders)
115
+ ('include_folders', config_options.Deprecated(
116
+ moved_to="folders", removed=False)),
117
+ #
118
+ # ### folders
111
119
  #
112
120
  # Directories whose name matches a glob pattern in this list will be
113
121
  # searched for documentation
114
- ('include_folders', config_options.Type(list, default=['*'])),
122
+ ('folders', config_options.Type(list, default=['*'])),
115
123
  #
116
- # ### ignore_folders
124
+ # ### ignore_folders (renamed)
125
+ #
126
+ # Renamed [ignore](#ignore)
127
+ ('ignore_folders', config_options.Deprecated(
128
+ moved_to="ignore", removed=False)),
129
+ #
130
+ # ### ignore
117
131
  #
118
132
  # Directories whose name matches a glob pattern in this list will NOT be
119
133
  # searched for documentation.
120
- ('ignore_folders', config_options.Type(list, default=[])),
134
+ ('ignore', config_options.Type(
135
+ list,
136
+ default=[
137
+ "venv",
138
+ ".cache/**",
139
+ ".devcontainer/**",
140
+ ".github/**",
141
+ ".vscode/**",
142
+ "**/__pycache__/**",
143
+ ".git/**",
144
+ "*.egg-info",
145
+ ])),
121
146
  #
122
- # ### ignore_hidden
147
+ # ### ignore_hidden (deprecated)
123
148
  #
124
149
  # Hidden directories will not be searched if this is true.
125
- ('ignore_hidden', config_options.Type(bool, default=True)),
150
+ ('ignore_hidden', config_options.Deprecated(
151
+ moved_to=None,
152
+ message="Common ignore files have been added to 'ignore' instead",
153
+ removed=False)),
126
154
  #
127
155
  # ### merge_docs_dir
128
156
  #
@@ -132,17 +160,35 @@ class SimplePlugin(BasePlugin):
132
160
  # the result.
133
161
  ('merge_docs_dir', config_options.Type(bool, default=True)),
134
162
  #
135
- # ### build_docs_dir
163
+ # ### build_docs_dir (renamed)
164
+ #
165
+ # Renamed [build_dir](#build_dir)
166
+ ('build_docs_dir', config_options.Deprecated(
167
+ moved_to="build_dir", removed=False)),
168
+ #
169
+ # ### build_dir
136
170
  #
137
171
  # If set, the directory where docs will be collated to be build.
138
172
  # Otherwise, the build docs directory will be a temporary directory.
139
- ('build_docs_dir', config_options.Type(str, default='')),
173
+ ('build_dir', config_options.Type(str, default='')),
174
+ #
175
+ # #### copy
140
176
  #
141
- # ### include_extensions
177
+ # If set, docs will be copied to the build_docs_dir.
178
+ # Otherwise, files will be used in place.
179
+ ('copy', config_options.Type(bool, default=False)),
180
+ #
181
+ # ### include_extensions (renamed)
182
+ #
183
+ # Renamed [include](#include)
184
+ ('include_extensions', config_options.Deprecated(
185
+ moved_to="include", message="", removed=False)),
186
+ #
187
+ # ### include
142
188
  #
143
189
  # Any file in the searched directories whose name contains a string in
144
190
  # this list will simply be copied to the generated documentation.
145
- ('include_extensions',
191
+ ('include',
146
192
  config_options.Type(
147
193
  list,
148
194
  default=[
@@ -260,16 +306,21 @@ class SimplePlugin(BasePlugin):
260
306
  # PY2 returns a byte string by default. The Unicode prefix ensures a
261
307
  # Unicode string is returned. And it makes MkDocs temp dirs easier to
262
308
  # identify.
263
- self.tmp_build_docs_dir = tempfile.mkdtemp(prefix="mkdocs_simple_")
309
+ self.tmp_build_dir = tempfile.mkdtemp(prefix="mkdocs_simple_")
264
310
  self.paths = None
265
311
  self.dirty = False
266
312
  self.last_build_time = None
267
313
 
268
- def on_startup(self, *, command, dirty: bool) -> None:
314
+ def on_startup(self,
315
+ *,
316
+ command: Literal['build',
317
+ 'gh-deploy',
318
+ 'serve'],
319
+ dirty: bool):
269
320
  """Configure the plugin on startup."""
270
321
  self.dirty = dirty
271
322
 
272
- def on_config(self, config, **kwargs):
323
+ def on_config(self, config: MkDocsConfig):
273
324
  """Update configuration to use a temporary build directory."""
274
325
  # Save the config for documentation
275
326
  default_config = dict((name, config_option.default)
@@ -285,57 +336,55 @@ class SimplePlugin(BasePlugin):
285
336
  config_site_dir = get_config_site_dir(config.config_file_path)
286
337
 
287
338
  # Set the build docs dir to tmp location if not set by user
288
- if not self.config['build_docs_dir'] and self.config['merge_docs_dir']:
289
- self.config['build_docs_dir'] = config['docs_dir']
339
+ if not self.config['build_dir'] and self.config['merge_docs_dir']:
340
+ self.config['build_dir'] = config['docs_dir']
290
341
  else:
291
- self.config['build_docs_dir'] = self.tmp_build_docs_dir
342
+ self.config['build_dir'] = self.tmp_build_dir
292
343
 
293
344
  utils.log.info(
294
- "mkdocs-simple-plugin: build_docs_dir: %s",
295
- self.config['build_docs_dir'])
345
+ "mkdocs-simple-plugin: build_dir: %s",
346
+ self.config['build_dir'])
296
347
 
297
348
  # Create build directory
298
- os.makedirs(self.config['build_docs_dir'], exist_ok=True)
349
+ os.makedirs(self.config['build_dir'], exist_ok=True)
299
350
  # Save original docs directory location
300
351
  self.orig_docs_dir = config['docs_dir']
301
352
  # Update the docs_dir with our temporary one if not merging
302
353
  if not self.config['merge_docs_dir']:
303
- config['docs_dir'] = self.config['build_docs_dir']
354
+ config['docs_dir'] = self.config['build_dir']
304
355
  # Add all markdown extensions to include list
305
- self.config['include_extensions'] = list(utils.markdown_extensions) + \
306
- self.config['include_extensions']
356
+ self.config['include'] = list(utils.markdown_extensions) + \
357
+ self.config['include']
307
358
 
308
359
  # Always ignore the output paths
309
360
  self.config["ignore_paths"] = [
310
361
  os.path.abspath(config_site_dir),
311
362
  os.path.abspath(config['site_dir']),
312
- os.path.abspath(self.config['build_docs_dir'])]
363
+ os.path.abspath(self.config['build_dir'])]
313
364
  if self.config['merge_docs_dir']:
314
365
  self.config["ignore_paths"].append(
315
366
  os.path.abspath(config['docs_dir']))
316
367
  return config
317
368
 
318
- def on_files(self, files: Files, *, config):
369
+ def on_files(self, files: Files, /, *,
370
+ config: MkDocsConfig):
319
371
  """Update files based on plugin settings."""
320
372
  # Configure simple
321
373
  simple = Simple(**self.config)
322
374
 
323
375
  # Save paths to add to watch if serving
324
- self.paths = simple.build_docs(self.dirty, self.last_build_time)
376
+ do_copy = self.config["copy"]
377
+ self.paths = simple.build_docs(
378
+ self.dirty, self.last_build_time, do_copy)
325
379
  self.last_build_time = time.time()
326
380
 
327
381
  if not self.config["merge_docs_dir"]:
328
382
  # If not merging, remove files that are from the docs dir
329
- # pylint: disable=protected-access
330
- for file in files._files[:]:
331
- if file.abs_src_path.startswith(
332
- os.path.abspath(config['docs_dir'])):
383
+ abs_docs_dir = os.path.abspath(config['docs_dir'])
384
+ for _, file in files.src_uris.items():
385
+ if file.abs_src_path.startswith(abs_docs_dir):
333
386
  files.remove(file)
334
387
 
335
- dedupe_files = {}
336
- for file in files:
337
- dedupe_files[file.abs_dest_path] = file
338
-
339
388
  for path in self.paths:
340
389
  file = File(
341
390
  src_dir=os.path.abspath(path.output_root),
@@ -343,17 +392,18 @@ class SimplePlugin(BasePlugin):
343
392
  dest_dir=config.site_dir,
344
393
  use_directory_urls=config["use_directory_urls"]
345
394
  )
346
- if file.abs_dest_path in dedupe_files:
347
- files.remove(dedupe_files[file.abs_dest_path])
395
+ if file.src_uri in files.src_uris:
396
+ files.remove(file)
348
397
  files.append(file)
349
398
  return files
350
399
 
351
- def on_serve(self, server, *, config, builder):
400
+ def on_serve(self, server: LiveReloadServer, /, *, config: MkDocsConfig,
401
+ builder: Callable):
352
402
  """Add files to watch server."""
353
403
  # don't watch the build directory
354
404
  # pylint: disable=protected-access
355
- if self.config["build_docs_dir"] in server._watched_paths:
356
- server.unwatch(self.config["build_docs_dir"])
405
+ if self.config["build_dir"] in server._watched_paths:
406
+ server.unwatch(self.config["build_dir"])
357
407
 
358
408
  # watch all the doc files
359
409
  for path in self.paths:
@@ -1,24 +1,72 @@
1
1
  """Semiliterate module handles document extraction from source files."""
2
+ from io import TextIOWrapper
2
3
  import os
3
4
  import re
4
5
 
5
- from mkdocs import utils
6
-
6
+ from dataclasses import dataclass
7
7
 
8
- def get_line(line: str) -> str:
9
- """Returns line with EOL."""
10
- if not line:
11
- return None
12
- return line if line.endswith("\n") else line + '\n'
8
+ from mkdocs import utils
13
9
 
14
10
 
15
- def get_match(pattern: re.Pattern, line: str) -> re.Match:
11
+ def _get_match(pattern: re.Pattern, line: str) -> re.Match:
16
12
  """Returns the match for the given pattern."""
17
13
  if not pattern:
18
14
  return None
19
15
  return pattern.search(line)
20
16
 
21
17
 
18
+ @dataclass
19
+ class InlineParams:
20
+ """Inline parameters for extraction."""
21
+ # md file=inline_params.snippet
22
+ # These parameters should be on the same line as the start block.
23
+ #
24
+ # For example:
25
+ #
26
+ # ```
27
+ # /**md file="new_name.md" trim=2 content="^\s*\/\/\s?(.*)$"
28
+ # ```
29
+ #
30
+ # #### Set output file name
31
+ #
32
+ # Filename is relative to the folder of the file being processed.
33
+ #
34
+ # ```
35
+ # file=<name>
36
+ # ```
37
+ filename_pattern: re.Pattern = re.compile(r"file=[\"']?(\w+.\w+)[\"']?\b")
38
+ filename: str = None
39
+ #
40
+ # #### Trim the front of a line
41
+ #
42
+ # Useful for removing leading spaces.
43
+ #
44
+ # ```
45
+ # trim=#
46
+ # ```
47
+ trim_pattern: re.Pattern = re.compile(r"trim=[\"']?(\d+)[\"']?\b")
48
+ trim: int = 0
49
+ # #### Capture content
50
+ #
51
+ # Regex expression to capture content, otherwise all lines are captured.
52
+ #
53
+ # ```
54
+ # content=<regex>
55
+ # ```
56
+ content_pattern: re.Pattern = re.compile(r"content=[\"']?([^\"']*)[\"']?")
57
+ content: str = None
58
+ #
59
+ # #### Stop capture
60
+ #
61
+ # Regex expression to indicate capture should stop.
62
+ #
63
+ # ```
64
+ # stop=<regex>
65
+ # ```
66
+ stop_pattern: re.Pattern = re.compile(r"stop=[\"']?([^\"']*)[\"']?")
67
+ # /md
68
+
69
+
22
70
  class ExtractionPattern:
23
71
  """An ExtractionPattern for a file."""
24
72
  # md file="ExtractionPattern.snippet"
@@ -89,90 +137,49 @@ class ExtractionPattern:
89
137
  else:
90
138
  self.replace.append((re.compile(item[0]), item[1]))
91
139
 
92
- # md file=inline_params.snippet
93
- # These parameters should be on the same line as the start block.
94
- #
95
- # For example:
96
- #
97
- # ```
98
- # /**md file="new_name.md" trim=2 content="^\s*\/\/\s?(.*)$"
99
- # ```
100
- #
101
- # #### Set output file name
102
- #
103
- # Filename is relative to the folder of the file being processed.
104
- #
105
- # ```
106
- # file=<name>
107
- # ```
108
- self._filename_pattern = re.compile(r"file=[\"']?(\w+.\w+)[\"']?\b")
109
- self._filename = None
110
- #
111
- # #### Trim the front of a line
112
- #
113
- # Useful for removing leading spaces.
114
- #
115
- # ```
116
- # trim=#
117
- # ```
118
- self._trim_pattern = re.compile(r"trim=[\"']?(\d+)[\"']?\b")
119
- self._trim = 0
120
- # #### Capture content
121
- #
122
- # Regex expression to capture content, otherwise all lines are captured.
123
- #
124
- # ```
125
- # content=<regex>
126
- # ```
127
- self._content_pattern = re.compile(r"content=[\"']?([^\"']*)[\"']?")
128
- self._content = None
129
- #
130
- # #### Stop capture
131
- #
132
- # Regex expression to indicate capture should stop.
133
- #
134
- # ```
135
- # stop=<regex>
136
- # ```
137
- self._stop_pattern = re.compile(r"stop=[\"']?([^\"']*)[\"']?")
138
- # /md
140
+ self.inline = InlineParams()
139
141
 
140
142
  def setup(self, line: str) -> None:
141
143
  """Process input parameters."""
142
- self._filename = None
143
- file_match = get_match(self._filename_pattern, line)
144
+ setup_inline = InlineParams()
145
+
146
+ file_match = _get_match(setup_inline.filename_pattern, line)
144
147
  if file_match and file_match.lastindex:
145
- self._filename = file_match[file_match.lastindex]
148
+ setup_inline.filename = file_match[file_match.lastindex]
146
149
 
147
- self._trim = 0
148
- trim_match = get_match(self._trim_pattern, line)
150
+ trim_match = _get_match(setup_inline.trim_pattern, line)
149
151
  if trim_match and trim_match.lastindex:
150
- self._trim = int(trim_match[trim_match.lastindex])
152
+ setup_inline.trim = int(trim_match[trim_match.lastindex])
151
153
 
152
- self._content = None
153
- content_match = get_match(self._content_pattern, line)
154
+ content_match = _get_match(setup_inline.content_pattern, line)
154
155
  if content_match and content_match.lastindex:
155
156
  regex_pattern = content_match[content_match.lastindex]
156
- self._content = re.compile(regex_pattern)
157
+ setup_inline.content = re.compile(regex_pattern)
157
158
 
159
+ # choose which stop option to use.
160
+ # Order by;
161
+ # 1. default from extraction pattern settings
162
+ # 2. default from inline params
158
163
  self.stop = self._stop_default
159
- stop_match = get_match(self._stop_pattern, line)
164
+ stop_match = _get_match(setup_inline.stop_pattern, line)
160
165
  if stop_match and stop_match.lastindex:
161
166
  regex_pattern = stop_match[stop_match.lastindex]
162
167
  self.stop = re.compile(regex_pattern)
163
168
 
169
+ self.inline = setup_inline
170
+
164
171
  def get_filename(self) -> str:
165
172
  """Returns the filename if defined in start arguments."""
166
- return self._filename
173
+ return self.inline.filename
167
174
 
168
175
  def replace_line(self, line: str) -> str:
169
176
  """Apply the specified replacements to the line and return it."""
170
177
  # Process trimming
171
- if self._trim:
172
- line = line[self._trim:]
178
+ if self.inline.trim:
179
+ line = line[self.inline.trim:]
173
180
  # Process inline content regex
174
- if self._content:
175
- match_object = get_match(self._content, line)
181
+ if self.inline.content:
182
+ match_object = _get_match(self.inline.content, line)
176
183
  if match_object.lastindex:
177
184
  return match_object[match_object.lastindex]
178
185
  # Preform replace operations
@@ -214,126 +221,132 @@ class LazyFile:
214
221
  return os.path.join(self.file_directory, self.file_name)
215
222
 
216
223
  def write(self, arg: str) -> None:
217
- """Create and write the file, only if not empty."""
218
- if not arg:
224
+ """Create and write a string line to the file, iff not none."""
225
+ if arg is None:
219
226
  return
220
227
  if self.file_object is None:
221
228
  filename = os.path.join(self.file_directory, self.file_name)
222
229
  os.makedirs(self.file_directory, exist_ok=True)
223
230
  self.file_object = open(filename, 'w+')
224
- self.file_object.write(arg)
231
+
232
+ def get_line(line: str) -> str:
233
+ """Returns line with EOL."""
234
+ return line if line.endswith("\n") else line + '\n'
235
+
236
+ self.file_object.write(get_line(arg))
225
237
 
226
238
  def close(self) -> str:
227
239
  """Finish the file."""
228
240
  if self.file_object is not None:
229
- file = os.path.join(self.file_directory, self.file_name)
230
- utils.log.debug(" ... extracted %s", file)
241
+ file_path = os.path.join(self.file_directory, self.file_name)
242
+ utils.log.debug(" ... extracted %s", file_path)
231
243
  self.file_object.close()
232
244
  self.file_object = None
233
- return file
245
+ return file_path
234
246
  return None
235
247
 
236
248
 
237
249
  class StreamExtract:
238
- """Extract documentation portions of files to an output stream."""
250
+ """Extract files to an output stream.
251
+
252
+ Optionally filter using a list of ExtractionPatterns.
253
+ """
239
254
 
240
255
  def __init__(
241
256
  self,
242
- input_stream: LazyFile,
257
+ input_stream: TextIOWrapper,
243
258
  output_stream: LazyFile,
244
259
  terminate: re.Pattern = None,
245
260
  patterns: ExtractionPattern = None,
246
261
  **kwargs):
247
262
  """Initialize StreamExtract with input and output streams."""
248
263
  self.input_stream = input_stream
249
- self.default_stream = output_stream
250
264
  self.output_stream = output_stream
251
265
  self.terminate = terminate
252
266
  self.patterns = patterns
253
- self.wrote_something = False
254
- self.output_files = []
255
- self.streams = {
267
+
268
+ self._default_stream = output_stream
269
+ self._output_files = []
270
+ self._streams = {
256
271
  output_stream.file_name: output_stream
257
272
  }
258
273
 
259
- def transcribe(self, text: str) -> None:
260
- """Write some text and record if something was written."""
261
- self.output_stream.write(text)
262
- if text:
263
- self.wrote_something = True
264
-
265
- def try_extract_match(
274
+ def _try_extract_match(
266
275
  self,
267
276
  match_object: re.Match,
268
277
  emit_last: bool = True) -> bool:
269
- """Extract match into output.
278
+ """Extracts line iff there's a match.
270
279
 
271
- If _match_object_ is not false-y, returns true.
272
- If extract flag is true, emits the last group of the match if any.
280
+ Returns:
281
+ True iff match_object exists.
273
282
  """
274
283
  if not match_object:
275
284
  return False
276
285
  if match_object.lastindex and emit_last:
277
- self.transcribe(get_line(match_object[match_object.lastindex]))
286
+ self.output_stream.write(match_object[match_object.lastindex])
278
287
  return True
279
288
 
280
289
  def close(self) -> list:
281
- """Returns true if something was written"""
290
+ """Close the file and return a list of filenames written to."""
282
291
  file = self.output_stream.close()
283
- if file and self.wrote_something:
284
- self.output_files.append(file)
285
- return self.output_files
292
+ if file:
293
+ self._output_files.append(file)
294
+ return self._output_files
286
295
 
287
- def set_output_file(self, filename: str) -> None:
288
- """Set output stream from filename."""
296
+ def set_output_file(self, filename: str) -> LazyFile:
297
+ """Set the current output stream from filename and return the stream."""
289
298
  output_stream = self.output_stream
290
299
  if filename:
291
300
  # If we've opened this file before, re-use its stream.
292
- if filename in self.streams:
293
- return self.set_output_stream(self.streams[filename])
301
+ if filename in self._streams:
302
+ return self.set_output_stream(self._streams[filename])
294
303
  # Otherwise, make a new one and save it to the list.
295
304
  output_stream = LazyFile(
296
305
  self.output_stream.file_directory, filename)
297
- self.streams[filename] = output_stream
298
- self.set_output_stream(output_stream)
306
+ self._streams[filename] = output_stream
307
+ return self.set_output_stream(output_stream)
299
308
 
300
- def set_output_stream(self, stream: LazyFile) -> None:
301
- """Set the output stream."""
309
+ def set_output_stream(self, stream: LazyFile) -> LazyFile:
310
+ """Set the current output stream and return the stream."""
302
311
  if self.output_stream != stream:
303
312
  self.close()
304
313
  self.output_stream = stream
314
+ return self.output_stream
305
315
 
306
316
  def extract(self, **kwargs) -> list:
307
317
  """Extract from file with semiliterate configuration.
308
318
 
309
- Invoke this method to perform the extraction. Returns true if
310
- any text is actually extracted, false otherwise.
319
+ Invoke this method to perform the extraction.
320
+
321
+ Returns:
322
+ A list of files extracted.
311
323
  """
312
324
  active_pattern = None if self.patterns else ExtractionPattern()
313
- for pattern in self.patterns:
325
+ patterns = self.patterns if self.patterns else []
326
+ for pattern in patterns:
314
327
  if not pattern.start:
315
328
  active_pattern = pattern
316
329
 
317
330
  for line in self.input_stream:
318
331
  # Check terminate, regardless of state:
319
- if self.try_extract_match(
320
- get_match(self.terminate, line), active_pattern):
332
+ if self._try_extract_match(
333
+ _get_match(self.terminate, line), active_pattern):
321
334
  return self.close()
322
335
  # Change state if flagged to do so:
323
336
  if active_pattern is None:
324
- for pattern in self.patterns:
325
- start = get_match(pattern.start, line)
337
+ for pattern in patterns:
338
+ start = _get_match(pattern.start, line)
326
339
  if start:
327
340
  active_pattern = pattern
328
341
  active_pattern.setup(line)
329
342
  self.set_output_file(active_pattern.get_filename())
330
- self.try_extract_match(start)
343
+ self._try_extract_match(start)
331
344
  break
332
345
  continue
333
346
  # We are extracting. See if we should stop:
334
- if self.try_extract_match(get_match(active_pattern.stop, line)):
347
+ if self._try_extract_match(_get_match(active_pattern.stop, line)):
335
348
  active_pattern = None
336
- self.set_output_stream(self.default_stream)
349
+ self.set_output_stream(self._default_stream)
337
350
  continue
338
351
  # Extract all other lines in the normal way:
339
352
  self.extract_line(line, active_pattern)
@@ -341,8 +354,8 @@ class StreamExtract:
341
354
 
342
355
  def extract_line(self, line: str, extraction_pattern: re.Pattern) -> None:
343
356
  """Copy line to the output stream, applying specified replacements."""
344
- line = get_line(extraction_pattern.replace_line(line))
345
- self.transcribe(line)
357
+ line = extraction_pattern.replace_line(line)
358
+ self.output_stream.write(line)
346
359
 
347
360
 
348
361
  class Semiliterate:
@@ -397,18 +410,25 @@ class Semiliterate:
397
410
  self.file_filter = re.compile(pattern)
398
411
  self.destination = destination
399
412
  self.terminate = (terminate is not None) and re.compile(terminate)
400
- self.patterns = []
413
+ self.extractions = []
401
414
  if not extract:
402
415
  extract = []
403
416
  if isinstance(extract, dict):
404
417
  # if there is only one extraction pattern, allow it to be a single
405
418
  # dict entry
406
419
  extract = [extract]
407
- for pattern in extract:
408
- self.patterns.append(ExtractionPattern(**pattern))
420
+ for extract_params in extract:
421
+ self.extractions.append(ExtractionPattern(**extract_params))
409
422
 
410
423
  def filename_match(self, name: str) -> str:
411
- """Get the filename for the match, otherwise return None."""
424
+ """Get the filename for the match, otherwise return None.
425
+
426
+ Args:
427
+ name (str): The name to match with the pattern filter
428
+
429
+ Returns:
430
+ The output filename for 'name' or None
431
+ """
412
432
  name_match = self.file_filter.search(name)
413
433
  if name_match:
414
434
  new_name = os.path.splitext(name)[0] + '.md'
@@ -425,7 +445,12 @@ class Semiliterate:
425
445
  **kwargs) -> list:
426
446
  """Try to extract documentation from file with name.
427
447
 
428
- Returns True if extraction was successful.
448
+ Args:
449
+ from_directory (str): The source directory
450
+ from_file (str): The source filename within directory
451
+ destination_directory (str): The destination directory
452
+
453
+ Returns a list of extracted files.
429
454
  """
430
455
  to_file = self.filename_match(from_file)
431
456
  if not to_file:
@@ -439,11 +464,11 @@ class Semiliterate:
439
464
  input_stream=original_file,
440
465
  output_stream=LazyFile(destination_directory, to_file),
441
466
  terminate=self.terminate,
442
- patterns=self.patterns,
467
+ patterns=self.extractions,
443
468
  **kwargs)
444
469
  return extraction.extract()
445
470
  except (UnicodeDecodeError) as error:
446
- utils.log.info("mkdocs-simple-plugin: Skipped %s", from_file_path)
471
+ utils.log.debug("mkdocs-simple-plugin: Skipped %s", from_file_path)
447
472
  utils.log.debug(
448
473
  "mkdocs-simple-plugin: Error details: %s", str(error))
449
474
  except (OSError, IOError) as error:
@@ -1,8 +1,8 @@
1
1
  """Simple module handles document extraction from source files."""
2
- import os
3
2
  import fnmatch
4
- import stat
3
+ import os
5
4
  import pathlib
5
+ import stat
6
6
 
7
7
  from shutil import copy2 as copy
8
8
  from dataclasses import dataclass
@@ -26,10 +26,10 @@ class Simple():
26
26
  # pylint: disable=too-many-instance-attributes
27
27
  def __init__(
28
28
  self,
29
- build_docs_dir: str,
30
- include_folders: list,
31
- include_extensions: list,
32
- ignore_folders: list,
29
+ build_dir: str,
30
+ folders: list,
31
+ include: list,
32
+ ignore: list,
33
33
  ignore_hidden: bool,
34
34
  ignore_paths: list,
35
35
  semiliterate: list,
@@ -37,23 +37,22 @@ class Simple():
37
37
  """Initialize module instance with settings.
38
38
 
39
39
  Args:
40
- build_docs_dir (str): Output directory for processed files
41
- include_folders (list): Glob of folders to search for files
42
- include_extensions (list): Glob of filenames to copy directly to
43
- output
44
- ignore_folders (list): Glob of paths to exclude
40
+ build_dir (str): Output directory for processed files
41
+ folders (list): Glob of folders to search for files
42
+ include (list): Glob of filenames to copy directly to output
43
+ ignore (list): Glob of paths to exclude
45
44
  ignore_hidden (bool): Whether to ignore hidden files for processing
46
45
  ignore_paths (list): Absolute filepaths to exclude
47
46
  semiliterate (list): Settings for processing file content in
48
47
  Semiliterate
49
48
 
50
49
  """
51
- self.build_dir = build_docs_dir
52
- self.include_folders = set(include_folders)
53
- self.copy_glob = set(include_extensions)
54
- self.ignore_glob = set(ignore_folders)
55
- self.ignore_hidden = ignore_hidden
56
- self.hidden_prefix = set([".", "__"])
50
+ self.build_dir = build_dir
51
+ self.folders = set(folders)
52
+ self.doc_glob = set(include)
53
+ self.ignore_glob = set(ignore)
54
+ self.ignore_hidden = ignore_hidden # to be deprecated
55
+ self.hidden_prefix = set([".", "__"]) # to be deprecated
57
56
  self.ignore_paths = set(ignore_paths)
58
57
  self.semiliterate = []
59
58
  for item in semiliterate:
@@ -64,7 +63,7 @@ class Simple():
64
63
  files = []
65
64
  # Get all of the entries that match the include pattern.
66
65
  entries = []
67
- for pattern in self.include_folders:
66
+ for pattern in self.folders:
68
67
  entries.extend(pathlib.Path().glob(pattern))
69
68
  # Ignore any entries that match the ignore pattern
70
69
  entries[:] = [
@@ -101,7 +100,7 @@ class Simple():
101
100
  mkdocsignore = os.path.join(base_path, ".mkdocsignore")
102
101
  if os.path.exists(mkdocsignore):
103
102
  ignore_list = []
104
- with open(mkdocsignore, "r") as txt_file:
103
+ with open(mkdocsignore, mode="r", encoding="utf-8") as txt_file:
105
104
  ignore_list = txt_file.read().splitlines()
106
105
  # Remove all comment lines
107
106
  ignore_list = [x for x in ignore_list if not x.startswith('#')]
@@ -115,12 +114,12 @@ class Simple():
115
114
  return True
116
115
  return False
117
116
 
118
- def should_copy_file(self, name: str) -> bool:
119
- """Check if file should be copied."""
117
+ def is_doc_file(self, name: str) -> bool:
118
+ """Check if file is a desired doc file."""
120
119
  def match_pattern(name, pattern):
121
120
  return fnmatch.fnmatch(name, pattern) or pattern in name
122
121
 
123
- return any(match_pattern(name, pattern) for pattern in self.copy_glob)
122
+ return any(match_pattern(name, pattern) for pattern in self.doc_glob)
124
123
 
125
124
  def should_extract_file(self, name: str):
126
125
  """Check if file should be extracted."""
@@ -142,13 +141,18 @@ class Simple():
142
141
  return any(name.startswith(pattern)
143
142
  for pattern in self.hidden_prefix)
144
143
  return any(hidden_prefix(part) for part in parts)
145
-
146
- extract = True
144
+ # Check if file is text based
145
+ try:
146
+ with open(name, 'r', encoding='utf-8') as f:
147
+ _ = f.read()
148
+ except UnicodeDecodeError:
149
+ return False
150
+
151
+ # Check if file is hidden and should ignore
147
152
  if self.ignore_hidden:
148
153
  is_hidden = has_hidden_prefix(name) or has_hidden_attribute(name)
149
- extract = not is_hidden
150
-
151
- return extract
154
+ return not is_hidden
155
+ return True
152
156
 
153
157
  def merge_docs(self, from_dir, dirty=False):
154
158
  """Merge docs directory"""
@@ -173,7 +177,11 @@ class Simple():
173
177
  source_file, destination_file)
174
178
  self.ignore_paths.add(from_dir)
175
179
 
176
- def build_docs(self, dirty=False, last_build_time=None) -> list:
180
+ def build_docs(
181
+ self,
182
+ dirty=False,
183
+ last_build_time=None,
184
+ do_copy=False) -> list:
177
185
  """Build the docs directory from workspace files."""
178
186
  paths = []
179
187
  files = self.get_files()
@@ -188,8 +196,9 @@ class Simple():
188
196
  build_prefix = os.path.normpath(
189
197
  os.path.join(self.build_dir, from_dir))
190
198
 
191
- copy_paths = self.try_copy_file(from_dir, name, build_prefix)
192
- if copy_paths:
199
+ doc_paths = self.get_doc_file(
200
+ from_dir, name, build_prefix, do_copy)
201
+ if doc_paths:
193
202
  paths.append(
194
203
  SimplePath(
195
204
  output_root=".",
@@ -231,17 +240,23 @@ class Simple():
231
240
 
232
241
  return []
233
242
 
234
- def try_copy_file(self, from_dir: str, name: str, to_dir: str) -> list:
243
+ def get_doc_file(
244
+ self,
245
+ from_dir: str,
246
+ name: str,
247
+ to_dir: str,
248
+ do_copy: bool) -> list:
235
249
  """Copy file with the same name to a new directory.
236
250
 
237
251
  Returns true if file copied.
238
252
  """
239
253
  original = os.path.join(from_dir, name)
240
- destination = os.path.join(to_dir, name)
241
254
 
242
- if not self.should_copy_file(os.path.join(from_dir, name)):
255
+ if not self.is_doc_file(os.path.join(from_dir, name)):
243
256
  return []
244
257
 
245
- os.makedirs(to_dir, exist_ok=True)
246
- copy(original, destination)
258
+ if do_copy:
259
+ destination = os.path.join(to_dir, name)
260
+ os.makedirs(to_dir, exist_ok=True)
261
+ copy(original, destination)
247
262
  return [original]
@@ -23,19 +23,18 @@ classifiers = [
23
23
  "Intended Audience :: Information Technology",
24
24
  "Programming Language :: Python",
25
25
  "Programming Language :: Python :: 3 :: Only",
26
- "Programming Language :: Python :: 3.7",
27
26
  "Programming Language :: Python :: 3.8",
28
27
  "Programming Language :: Python :: 3.9",
29
28
  "Programming Language :: Python :: 3.10",
30
29
  "Programming Language :: Python :: 3.11"
31
30
  ]
32
31
  # md file="versions.snippet"
33
- # _Python 3.x, 3.7, 3.8, 3.9, 3.10, 3.11 supported._
32
+ # _Python 3.x, 3.8, 3.9, 3.10, 3.11 supported._
34
33
  # /md
35
34
  dependencies = [
36
35
  "click>=7.1",
37
36
  "MarkupSafe>=2.1.1",
38
- "mkdocs>=1.4.0",
37
+ "mkdocs>=1.6.0",
39
38
  "PyYAML>=6.0",
40
39
  ]
41
40
 
@@ -1,42 +0,0 @@
1
- # Package Guide
2
-
3
- ## Prerequisites
4
-
5
- {% include "setup.snippet" %}
6
-
7
-
8
- ## Building
9
-
10
- {% include "build.snippet" %}
11
-
12
- ## Testing
13
-
14
- {% include "tests/linters.snippet" %}
15
-
16
- {% include "tests/unit_tests.snippet" %}
17
-
18
- {% include "tests/integration_tests.snippet" %}
19
-
20
- {% include "tests/local_tests.snippet" %}
21
-
22
- ## VSCode
23
-
24
- Included in this package is a VSCode workspace and development container. See [how I develop with VSCode and Docker](https://allisonthackston.com/articles/docker-development.html) and [how I use VSCode tasks](https://allisonthackston.com/articles/vscode-tasks.html).
25
-
26
- ## Packaging
27
-
28
- [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
29
-
30
- The project uses Hatch to build and package the plugin
31
-
32
- ### Build the package
33
-
34
- ```bash
35
- hatch build
36
- ```
37
-
38
- ### Publish the package
39
-
40
- ```bash
41
- hatch publish
42
- ```