Skip to content

Functions

src.utils.build.generate_version_file(version: str)

Generate version file using the version provided.

Parameters:

Name Type Description Default
version str

Version string to use in the version file.

required
Source code in src\utils\build.py
76
77
78
79
80
81
82
83
def generate_version_file(version: str):
    """Generate version file using the version provided.

    Args:
        version: Version string to use in the version file.
    """
    with open(Path(SRC, '__VERSION__.py'), 'w') as f:
        f.write(f"version='{version}'")

src.utils.build.make_directories(config: DistConfig) -> None

Make sure necessary directories exist.

Parameters:

Name Type Description Default
config DistConfig

Config data from 'dist.yml'.

required
Source code in src\utils\build.py
86
87
88
89
90
91
92
93
94
def make_directories(config: DistConfig) -> None:
    """Make sure necessary directories exist.

    Args:
        config: Config data from 'dist.yml'.
    """
    DST.mkdir(mode=777, parents=True, exist_ok=True)
    for path in config['make']['paths']:
        Path(DST, *path).mkdir(mode=777, parents=True, exist_ok=True)

src.utils.build.copy_directory(src: Union[str, os.PathLike], dst: Union[str, os.PathLike], x_files: list[str], x_dirs: list[str], x_ext: Optional[list[str]] = None, recursive: bool = True) -> None

Copy a directory from src to dst.

Parameters:

Name Type Description Default
src str | PathLike

Source directory to copy this directory from.

required
dst str | PathLike

Destination directory to copy this directory to.

required
x_files list[str]

Excluded file names.

required
x_dirs list[str]

Excluded directory names.

required
x_ext list[str] | None

Excluded extensions.

None
recursive bool

Will exclude all subdirectories if False.

True
Source code in src\utils\build.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def copy_directory(
    src: Union[str, os.PathLike],
    dst: Union[str, os.PathLike],
    x_files: list[str],
    x_dirs: list[str],
    x_ext: Optional[list[str]] = None,
    recursive: bool = True
) -> None:
    """Copy a directory from src to dst.

    Args:
        src: Source directory to copy this directory from.
        dst: Destination directory to copy this directory to.
        x_files: Excluded file names.
        x_dirs: Excluded directory names.
        x_ext: Excluded extensions.
        recursive: Will exclude all subdirectories if False.
    """
    # Set empty lists for None value
    x_files = x_files or []
    x_dirs = x_dirs or []
    x_ext = x_ext or []

    def _ignore(path: str, names: list[str]) -> set[str]:
        """Return a list of files to ignore based on our exclusion criteria.

        Args:
            path: Path to these files.
            names: Names of these files.
        """
        ignored: list[str] = []
        for name in names:
            # Ignore certain names and extensions
            p = Path(path, name)
            if name in x_files or p.suffix in x_ext:
                ignored.append(name)
            # Ignore certain directories
            elif (name in x_dirs or not recursive) and p.is_dir():
                ignored.append(name)
        return set(ignored)

    # Copy the directory
    copytree(src, dst, ignore=_ignore, dirs_exist_ok=True)

src.utils.build.copy_app_files(config: DistConfig) -> None

Copy necessary app files and directories.

Parameters:

Name Type Description Default
config DistConfig

Config data from 'dist.yml'.

required
Source code in src\utils\build.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def copy_app_files(config: DistConfig) -> None:
    """Copy necessary app files and directories.

    Args:
        config: Config data from 'dist.yml'.
    """
    for _, DIR in config.get('copy', {}).items():
        # Copy directories
        for path in DIR.get('paths', []):
            copy_directory(
                src=Path(SRC, *path),
                dst=Path(DST, *path),
                x_files=DIR.get('exclude_files', []),
                x_dirs=DIR.get('exclude_dirs', []),
                x_ext=DIR.get('exclude_ext', []),
                recursive=bool(DIR.get('recursive', True)))
        # Copy files
        for file in DIR.get('files', []):
            copy2(
                src=Path(SRC, *file),
                dst=Path(DST, *file))

src.utils.build.clear_build_files(clear_dist: bool = True) -> None

Clean out pycache and venv cache, remove previous build files.

Parameters:

Name Type Description Default
clear_dist bool

Remove previous dist directory if True, otherwise skip.

True
Source code in src\utils\build.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def clear_build_files(clear_dist: bool = True) -> None:
    """Clean out __pycache__ and venv cache, remove previous build files.

    Args:
        clear_dist: Remove previous dist directory if True, otherwise skip.
    """
    # Run pyclean on main directory and venv
    os.system("pyclean -v .")
    if os.path.exists(os.path.join(SRC, '.venv')):
        os.system("pyclean -v .venv")

    # Remove build directory
    with suppress(Exception):
        rmtree(os.path.join(SRC, 'build'))

    # Optionally remove dist directory
    if clear_dist:
        with suppress(Exception):
            rmtree(os.path.join(SRC, 'dist'))

src.utils.build.build_zip(filename: str) -> None

Create a zip of this release.

Parameters:

Name Type Description Default
filename str

Filename to use on zip archive.

required
Source code in src\utils\build.py
196
197
198
199
200
201
202
203
204
205
206
207
def build_zip(filename: str) -> None:
    """Create a zip of this release.

    Args:
        filename: Filename to use on zip archive.
    """
    ZIP_SRC = os.path.join(SRC, filename)
    with zipfile.ZipFile(ZIP_SRC, "w", zipfile.ZIP_DEFLATED) as zipf:
        for fp in glob(os.path.join(DST, "**/*"), recursive=True):
            zipf.write(fp, arcname=fp.replace(
                os.path.commonpath([DST, fp]), ""))
    move(ZIP_SRC, os.path.join(DST, filename))

src.utils.build.build_release(version: Optional[str] = None, console: bool = False, beta: bool = False, zipped: bool = True) -> None

Build the app to executable release.

Parameters:

Name Type Description Default
version str | None

Version to use in zip name and GUI display.

None
console bool

Whether to enable console window when app is launched.

False
beta bool

Whether this is a beta release.

False
zipped bool

Whether to create a zip of this release.

True
Source code in src\utils\build.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def build_release(
    version: Optional[str] = None,
    console: bool = False,
    beta: bool = False,
    zipped: bool = True
) -> None:
    """Build the app to executable release.

    Args:
        version: Version to use in zip name and GUI display.
        console: Whether to enable console window when app is launched.
        beta: Whether this is a beta release.
        zipped: Whether to create a zip of this release.
    """
    # Load dist config
    dist_config: DistConfig = load_data_file(DIST_CONFIG)

    # Pre-build steps
    clear_build_files()
    make_directories(dist_config)

    # Use provided version or fallback to project defined
    version = version or get_app_version((SRC / 'pyproject').with_suffix('.toml'))
    generate_version_file(version)

    # Run Pyinstaller
    spec_path: list[str] = dist_config['spec']['console'] if console else dist_config['spec']['release']
    PyInstaller.__main__.run([str(Path(SRC, *spec_path)), '--clean'])

    # Copy our essential app files
    copy_app_files(dist_config)

    # Build zip release if requested
    if zipped:
        build_zip(
            filename=dist_config['names']['zip'].format(
                version=version,
                console='-console' if console else '',
                beta='-beta' if beta else ''
            ))

    # Clear build files, except dist
    clear_build_files(clear_dist=False)
    os.remove(Path(SRC, '__VERSION__.py'))

src.utils.build.get_python_modules(path: Path) -> list[str]

Get a list of python files within a directory.

Parameters:

Name Type Description Default
path Path

Python module directory.

required

Returns:

Type Description
list[str]

List of module names.

Source code in src\utils\build.py
261
262
263
264
265
266
267
268
269
270
271
272
273
def get_python_modules(path: Path) -> list[str]:
    """Get a list of python files within a directory.

    Args:
        path: Python module directory.

    Returns:
        List of module names.
    """
    return [
        f[:-3] for f in os.listdir(path) if
        f.endswith('.py') and f != "__init__.py"
    ]

src.utils.build.generate_mkdocs(path: str) -> None

Generates a markdown file for each submodule in a python module directory.

Parameters:

Name Type Description Default
path str

Path to a python module directory.

required
Source code in src\utils\build.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def generate_mkdocs(path: str) -> None:
    """Generates a markdown file for each submodule in a python module directory.

    Args:
        path: Path to a python module directory.
    """
    directory = SRC / 'src' / path
    parent = 'temps' if path == 'templates' else path
    for module in get_python_modules(directory):
        functions, classes = [], []

        # Scan for functions and classes to document
        with open(Path(directory, module).with_suffix('.py')) as file:
            for node in ast.parse(file.read()).body:
                if isinstance(node, ast.FunctionDef):
                    functions.append(f"src.{path}.{module}.{node.name}")
                elif isinstance(node, ast.ClassDef):
                    classes.append(f"src.{path}.{module}.{node.name}")

        # Write MD file
        with open(
            Path(SRC, 'docs', parent, module).with_suffix('.md'),
            "w", encoding='utf-8'
        ) as f:
            if module[0] == '_':
                module = module[1:]
            module = module.title().replace('_', ' ')
            f.write(f"# {module}\n")
            if classes:
                [f.write(
                    f"\n::: {cls}\n"
                    f"    options:\n"
                    f"        show_root_members_full_path: false\n"
                    f"        show_category_heading: true\n"
                    f"        show_root_full_path: false\n"
                    f"        show_root_heading: true\n"
                ) for cls in classes]
            if functions:
                [f.write(
                    f"\n::: {func}\n"
                    f"    options:\n"
                    f"        show_root_members_full_path: false\n"
                    f"        show_category_heading: true\n"
                    f"        show_root_full_path: false\n"
                    f"        show_root_heading: true\n"
                ) for func in functions]

src.utils.build.generate_nav(headers: list[str], paths: list[str]) -> list[dict]

Generates the nav menu data for mkdocs.yml containing scanned modules.

Parameters:

Name Type Description Default
headers list[str]

Displayed nav category headers.

required
paths list[str]

Directory path names for each section.

required

Returns:

Type Description
list[dict]

List of nav item objects.

Source code in src\utils\build.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def generate_nav(headers: list[str], paths: list[str]) -> list[dict]:
    """Generates the nav menu data for mkdocs.yml containing scanned modules.

    Args:
        headers: Displayed nav category headers.
        paths: Directory path names for each section.

    Returns:
        List of nav item objects.
    """
    nav = []
    for i, path in enumerate(paths):
        parent = 'temps' if path == 'templates' else path
        md_files = sorted([f for f in os.listdir(Path(SRC, 'docs', parent)) if f.endswith('.md')])
        nav_items = [f'{parent}/{f}' for f in md_files]  # remove .md extension
        nav.append({headers[i]: nav_items})
    return nav

src.utils.build.update_mkdocs_yml(nav: list[dict]) -> None

Updates the mkdocs.yml file with the new nav list.

Parameters:

Name Type Description Default
nav list[dict]

List of nav objects to insert into nav data in mkdocs.yml.

required
Source code in src\utils\build.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def update_mkdocs_yml(nav: list[dict]) -> None:
    """Updates the mkdocs.yml file with the new nav list.

    Args:
        nav: List of nav objects to insert into nav data in mkdocs.yml.
    """
    mkdocs_yml = load_data_file(Path(SRC, 'mkdocs.yml'))
    mkdocs_yml['nav'] = [
        {'Home': 'index.md'},
        {'Changelog': 'changelog.md'},
        {'Reference': [
            *nav,
            {'Text Layer Classes': 'text_layers.md'},
            {'Card Layouts': 'layouts.md'}
        ]},
        {'License': 'license.md'}
    ]
    dump_data_file(mkdocs_yml, Path(SRC, 'mkdocs.yml'), config={'sort_keys': False})