diff --git a/CHANGELOG.md b/CHANGELOG.md index 87af0eab..9aedca1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ Instructions: Add a subsection under `[Unreleased]` for additions, fixes, change ## [Unreleased] +# Added + +- You can now specify the debug level *after* the command (e.g., `pretext -v debug build` can now also be entered as `pretext build -v debug`). +- Temporary directories created by core pretext will now be cleaned up at all debug levels. To save them for debugging, use the `--save-tmp-dirs` flag, as in `pretext --save-tmp-dirs build`. + ## [2.41.2] - 2026-06-07 Includes updates to core through commit: [fc8221d](https://github.com/PreTeXtBook/pretext/commit/fc8221d5b8e7027f686729102e169c482fbd1fe5) diff --git a/pretext/__init__.py b/pretext/__init__.py index 373779ad..f1b43c6a 100644 --- a/pretext/__init__.py +++ b/pretext/__init__.py @@ -19,7 +19,7 @@ VERSION = get_version("pretext", Path(__file__).parent.parent) -CORE_COMMIT = "fc8221d5b8e7027f686729102e169c482fbd1fe5" +CORE_COMMIT = "7bef656c9f303489fc07d719df52108c1970ca26" def activate() -> None: diff --git a/pretext/cli.py b/pretext/cli.py index d182c00b..b35e10a1 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -80,9 +80,48 @@ def try_except(ctx: click.Context, *args: Any, **kwargs: Any) -> Any: CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +_VERBOSITY_HELP = "Sets the severity of log messaging: DEBUG for all, INFO (default) for most, then WARNING, ERROR, and CRITICAL for decreasing verbosity." + + +def set_log_level( + _ctx: click.Context, _param: click.Parameter, value: Optional[str] +) -> None: + _ = (_ctx, _param) # Click's callback protocol requires these; not used here. + if value: + log.setLevel(value.upper()) + + +class PreTeXtGroup(click.Group): + """Click Group that injects a -v/--verbosity option into every subcommand so that + `pretext build -v debug` works in addition to the canonical `pretext -v debug build`. + """ + + def command(self, *args: Any, **kwargs: Any) -> Any: + original_decorator = super().command(*args, **kwargs) + + def decorator(f: Callable[..., Any]) -> Any: + f = click.option( + "--verbosity", + "-v", + default=None, + expose_value=False, + is_eager=True, + callback=set_log_level, + type=click.Choice( + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + case_sensitive=False, + ), + help=_VERBOSITY_HELP, + )(f) + return original_decorator(f) + + return decorator + # Click command-line interface -@click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS) +@click.group( + cls=PreTeXtGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS +) @click.pass_context # Allow a verbosity command: @click_log.simple_verbosity_option( @@ -96,8 +135,13 @@ def try_except(ctx: click.Context, *args: Any, **kwargs: Any) -> Any: is_flag=True, help='Display list of build/view "targets" available in the project manifest.', ) +@click.option( + "--save-tmp-dirs", + is_flag=True, + help="Do not clean temporary directories after a build or generate. Might be useful for debugging (previously calling -v debug would also save these directories).", +) @nice_errors -def main(ctx: click.Context, targets: bool) -> None: +def main(ctx: click.Context, targets: bool, save_tmp_dirs: bool) -> None: """ Command line tools for quickly creating, authoring, and building PreTeXt projects. @@ -177,7 +221,7 @@ def main(ctx: click.Context, targets: bool) -> None: f"No project.ptx manifest found in current workspace. Using global configuration specified in '~/.ptx/{VERSION}/project.ptx'." ) # Add project to context so it can be used in subcommands - ctx.obj = {"project": project} + ctx.obj = {"project": project, "save_tmp_dirs": save_tmp_dirs} @main.result_callback() @@ -552,6 +596,7 @@ def build( # Create a new project, apply overlay, and get target. Note, the CLI always finds changes to the root folder of the project, so we don't need to specify a path to the project.ptx file. # Use the project discovered in the main command. project = ctx.obj["project"] + save_tmp_dirs = ctx.obj.get("save_tmp_dirs", False) # Check to see whether target_name is a path to a file: if target_name and Path(target_name).is_file(): @@ -620,7 +665,11 @@ def build( for t in targets: log.info(f"Generating assets for {t.name}") t.generate_assets( - only_changed=False, xmlid=xmlid, clean=clean, skip_cache=True + only_changed=False, + xmlid=xmlid, + clean=clean, + skip_cache=True, + clean_tmp_dirs=not save_tmp_dirs, ) no_generate = True except Exception as e: @@ -645,6 +694,7 @@ def build( xmlid=xmlid, no_knowls=no_knowls, latex=latex, + clean_tmp_dirs=not save_tmp_dirs, ) if t.format == "html" and t.compression is None: log.info( @@ -761,6 +811,7 @@ def generate( return project = ctx.obj["project"] + save_tmp_dirs = ctx.obj.get("save_tmp_dirs", False) # Now create the target if the target_name is not missing. try: target = project.get_target(name=target_name) @@ -780,6 +831,7 @@ def generate( clean=clean, skip_cache=force, slow=slow, + clean_tmp_dirs=not save_tmp_dirs, ) # Check if there are errors reported by the build by looking at the error_flush_handler. if utils.has_errors(error_flush_handler): diff --git a/pretext/project/__init__.py b/pretext/project/__init__.py index d3d96182..776788e5 100644 --- a/pretext/project/__init__.py +++ b/pretext/project/__init__.py @@ -582,7 +582,7 @@ def save_asset_table(self, asset_table: pt.AssetTable) -> None: ) as f: json.dump(asset_table, f) - def ensure_myopenmath_xml(self) -> None: + def ensure_myopenmath_xml(self, clean_tmp_dirs: bool = True) -> None: """ Ensures that the myopenmath xml files are present if the source contains myopenmath exercises. Needed to generate other "static" assets and targets. """ @@ -607,14 +607,16 @@ def ensure_myopenmath_xml(self) -> None: f"MyOpenMath problem {prob_num} does not exist, generating" ) self.generate_assets( - requested_asset_types=["myopenmath"], only_changed=False + requested_asset_types=["myopenmath"], + only_changed=False, + clean_tmp_dirs=clean_tmp_dirs, ) # Only need to generate once a single missing file is discovered. break else: log.debug("Source does not contain myopenmath problems") - def ensure_webwork_reps(self) -> None: + def ensure_webwork_reps(self, clean_tmp_dirs: bool = True) -> None: """ Ensures that the webwork representation file is present if the source contains webwork problems. This is needed to build or generate other assets. """ @@ -634,7 +636,9 @@ def ensure_webwork_reps(self) -> None: f'At least one WeBWorK representation file (for webwork problem with id "{id}") does not exist, generating' ) self.generate_assets( - requested_asset_types=["webwork"], only_changed=False + requested_asset_types=["webwork"], + only_changed=False, + clean_tmp_dirs=clean_tmp_dirs, ) break else: @@ -720,6 +724,7 @@ def build( xmlid: t.Optional[str] = None, no_knowls: bool = False, latex: bool = False, + clean_tmp_dirs: bool = True, ) -> None: # Add cli.version to stringparams. Use only the major and minor version numbers. self.stringparams["cli.version"] = VERSION[: VERSION.rfind(".")] @@ -744,11 +749,11 @@ def build( self.ensure_asset_directories() # verify that a webwork_representations.xml file exists if it is needed; generated if needed. - self.ensure_webwork_reps() + self.ensure_webwork_reps(clean_tmp_dirs=clean_tmp_dirs) # Generate needed assets unless requested not to. if generate: - self.generate_assets(xmlid=xmlid) + self.generate_assets(xmlid=xmlid, clean_tmp_dirs=clean_tmp_dirs) # Ensure the output directories exist. self.ensure_output_directory() @@ -936,7 +941,7 @@ def build( log.critical(f"Unknown format {self.format}") # Delete temporary directories left behind by core: try: - core.release_temporary_directories() + core.release_temporary_directories(any_log_level=clean_tmp_dirs) except Exception as e: log.error( "Unable to release temporary directories. Please report this error to pretext-support" @@ -952,6 +957,7 @@ def generate_assets( clean: bool = False, skip_cache: bool = False, slow: bool = False, + clean_tmp_dirs: bool = True, ) -> None: """ Generates assets for the current target. Options: @@ -1320,11 +1326,9 @@ def generate_assets( # log.debug(e, exc_info=True) # Delete temporary directories left behind by core: try: - core.release_temporary_directories() + core.release_temporary_directories(any_log_level=clean_tmp_dirs) except Exception as e: - log.error( - "Unable to release temporary directories. Please report this error to pretext-support" - ) + log.debug("Unable to release temporary directories.") log.debug(e, exc_info=True) # After all assets are generated, update the asset cache (but we shouldn't do this if we didn't generate any assets successfully) log.debug(f"Updated these assets successfully: {successful_assets}")