diff --git a/.gitignore b/.gitignore index 7620630..621bd30 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,6 @@ __pycache__ *egg* __pycache__ *.dSYM -docs .mypy_cache/ -venv \ No newline at end of file +venv +site \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e3ed24..c313b47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,20 @@ # pygdbmi release history -## dev +## 0.9.0.3 + * Drop support for 2.7, 3.4 * Add support for 3.7, 3.8 +* Add `py.typed` file so mypy can enforce type hints on `pygdbmi` * Do not log in StringStream (#36) +* Updates to build and CI tests (use nox) +* Use mkdocs and mkdocstrings +* Doc updates + +## 0.9.0.2 +* More doc updates + +## 0.9.0.1 +* Update docs ## 0.9.0.0 * Stop buffering output diff --git a/MANIFEST.in b/MANIFEST.in index 82f3bc5..5f4664f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,12 @@ include *.md include LICENSE +graft pygdbmi + exclude example.py exclude .flake8 exclude noxfile.py +exclude mkdocs.yml prune tests prune docs \ No newline at end of file diff --git a/README.md b/README.md index b5c451e..d5a290a 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ pygdbmi - Get Structured Output from GDB's Machine Interface - - - +PyPI version + +

-**Documentation** https://cs01.github.io/pygdbmi +**Documentation** [https://cs01.github.io/pygdbmi](https://cs01.github.io/pygdbmi) -**Source Code** https://github.com/cs01/pygdbmi +**Source Code** [https://github.com/cs01/pygdbmi](https://github.com/cs01/pygdbmi) --- @@ -158,11 +158,13 @@ Documentation fixes, bug fixes, performance improvements, and functional improve pygdbmi uses [nox](https://github.com/theacodes/nox) for automation. See available tasks with + ``` nox -l ``` Run tests and lint with + ``` nox -s tests nox -s lint @@ -177,4 +179,4 @@ nox -s lint ## Authors -`pygdbmi` was written by [Chad Smith](https://grassfedcode.com) with [contributions from the community](https://github.com/cs01/pygdbmi/graphs/contributors) for which the author is very grateful. Thanks especially to @mariusmue, @bobthekingofegypt, @mouuff, and @felipesere. +`pygdbmi` was written by [Chad Smith](https://grassfedcode.com) with [contributions from the community](https://github.com/cs01/pygdbmi/graphs/contributors). Thanks especially to @mariusmue, @bobthekingofegypt, @mouuff, and @felipesere. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 120000 index 0000000..04c99a5 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/docs/api/gdbcontroller.md b/docs/api/gdbcontroller.md new file mode 100644 index 0000000..5ac8c66 --- /dev/null +++ b/docs/api/gdbcontroller.md @@ -0,0 +1 @@ +::: pygdbmi.gdbcontroller \ No newline at end of file diff --git a/docs/api/gdbmiparser.md b/docs/api/gdbmiparser.md new file mode 100644 index 0000000..58124da --- /dev/null +++ b/docs/api/gdbmiparser.md @@ -0,0 +1 @@ +::: pygdbmi.gdbmiparser \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..06d4082 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,22 @@ +site_name: pygdbmi +site_description: Parse gdb machine interface output with Python + +theme: + name: "material" +repo_name: cs01/pygdbmi +repo_url: https://github.com/cs01/pygdbmi + +nav: + - Home: "README.md" + - Api: + - "gdbmiparser": "api/gdbmiparser.md" + - "gdbcontroller": "api/gdbcontroller.md" + - Changelog: "CHANGELOG.md" + +markdown_extensions: + - admonition # note blocks, warning blocks -- https://github.com/mkdocs/mkdocs/issues/1659 + - codehilite + +plugins: + - mkdocstrings + - search diff --git a/noxfile.py b/noxfile.py index 017460a..2315bb3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -22,14 +22,25 @@ def lint(session): session.run("python", "setup.py", "check", "--metadata", "--strict") +doc_dependencies = [ + ".", + "git+https://github.com/cs01/mkdocstrings.git", + "mkdocs", + "mkdocs-material", + "pygments", +] + + @nox.session(python="3.7") def docs(session): - session.install(".", "pdoc3") - session.run( - "pdoc", "--html", "--force", "--output-dir", "/tmp/pygdbmi_docs", "pygdbmi" - ) - shutil.rmtree("docs", ignore_errors=True) - shutil.move("/tmp/pygdbmi_docs/pygdbmi", "docs") + session.install(*doc_dependencies) + session.run("mkdocs", "build") + + +@nox.session(python="3.7") +def serve_docs(session): + session.install(*doc_dependencies) + session.run("mkdocs", "serve") @nox.session(python="3.7") @@ -55,3 +66,4 @@ def publish(session): build(session) print("REMINDER: Has the changelog been updated?") session.run("python", "-m", "twine", "upload", "dist/*") + session.notify(publish_docs) diff --git a/pygdbmi/__init__.py b/pygdbmi/__init__.py index 5288bcd..e3d78fd 100644 --- a/pygdbmi/__init__.py +++ b/pygdbmi/__init__.py @@ -1,8 +1 @@ -""" -.. include:: ../README.md -""" -__title__ = "pygdbmi" -__version__ = "0.9.0.2" -__author__ = "Chad Smith" -__copyright__ = "Copyright Chad Smith" -__pdoc__ = {"StringStream": False, "printcolor": False} +__version__ = "0.9.0.3" diff --git a/pygdbmi/gdbcontroller.py b/pygdbmi/gdbcontroller.py index eb48519..9179d8b 100644 --- a/pygdbmi/gdbcontroller.py +++ b/pygdbmi/gdbcontroller.py @@ -1,4 +1,7 @@ -"""GdbController class to programatically run gdb and get structured output""" +"""This module defines the `GdbController` class +which runs gdb as a subprocess and can write to it and read from it to get +structured output. +""" import logging import os @@ -49,20 +52,6 @@ class GdbTimeoutError(ValueError): class GdbController: - """ - Run gdb as a subprocess. Send commands and receive structured output. - Create new object, along with a gdb subprocess - - Args: - gdb_path (str): Command to run in shell to spawn new gdb subprocess - gdb_args (list): Arguments to pass to shell when spawning new gdb subprocess - time_to_check_for_additional_output_sec (float): When parsing responses, wait this amout of time before exiting (exits before timeout is reached to save time). If <= 0, full timeout time is used. - rr (bool): Use the `rr replay` command instead of `gdb`. See rr-project.org for more info. - verbose (bool): Print verbose output if True - Returns: - New GdbController object - """ - def __init__( self, gdb_path: str = "gdb", @@ -71,6 +60,20 @@ def __init__( rr: bool = False, verbose: bool = False, ): + """ + Run gdb as a subprocess. Send commands and receive structured output. + Create new object, along with a gdb subprocess + + Args: + gdb_path: Command to run in shell to spawn new gdb subprocess + gdb_args: Arguments to pass to shell when spawning new gdb subprocess + time_to_check_for_additional_output_sec: When parsing responses, wait this amout of time before exiting (exits before timeout is reached to save time). If <= 0, full timeout time is used. + rr: Use the `rr replay` command instead of `gdb`. See rr-project.org for more info. + verbose: Print verbose output if True + Returns: + New GdbController object + """ + if gdb_args is None: default_gdb_args = ["--nx", "--quiet", "--interpreter=mi2"] gdb_args = default_gdb_args @@ -187,16 +190,16 @@ def write( """Write to gdb process. Block while parsing responses from gdb for a maximum of timeout_sec. Args: - mi_cmd_to_write (str or list): String to write to gdb. If list, it is joined by newlines. - timeout_sec (float): Maximum number of seconds to wait for response before exiting. Must be >= 0. - raise_error_on_timeout (bool): If read_response is True, raise error if no response is received - read_response (bool): Block and read response. If there is a separate thread running, + mi_cmd_to_write: String to write to gdb. If list, it is joined by newlines. + timeout_sec: Maximum number of seconds to wait for response before exiting. Must be >= 0. + raise_error_on_timeout: If read_response is True, raise error if no response is received + read_response: Block and read response. If there is a separate thread running, this can be false, and the reading thread read the output. Returns: List of parsed gdb responses if read_response is True, otherwise [] Raises: - NoGdbProcessError if there is no gdb subprocess running - TypeError if mi_cmd_to_write is not valid + NoGdbProcessError: if there is no gdb subprocess running + TypeError: if mi_cmd_to_write is not valid """ self.verify_valid_gdb_subprocess() if timeout_sec < 0: @@ -254,17 +257,17 @@ def get_gdb_response( by timeout_sec, an exception is raised. Args: - timeout_sec (float): Maximum time to wait for reponse. Must be >= 0. Will return after - raise_error_on_timeout (bool): Whether an exception should be raised if no response was found after timeout_sec + timeout_sec: Maximum time to wait for reponse. Must be >= 0. Will return after + raise_error_on_timeout: Whether an exception should be raised if no response was found after timeout_sec Returns: List of parsed GDB responses, returned from gdbmiparser.parse_response, with the additional key 'stream' which is either 'stdout' or 'stderr' Raises: - GdbTimeoutError if response is not received within timeout_sec - ValueError if select returned unexpected file number - NoGdbProcessError if there is no gdb subprocess running + GdbTimeoutError: if response is not received within timeout_sec + ValueError: if select returned unexpected file number + NoGdbProcessError: if there is no gdb subprocess running """ self.verify_valid_gdb_subprocess() @@ -404,12 +407,16 @@ def _get_responses_list(self, raw_output, stream): def send_signal_to_gdb(self, signal_input): """Send signal name (case insensitive) or number to gdb subprocess - gdbmi.send_signal_to_gdb(2) # valid - gdbmi.send_signal_to_gdb('sigint') # also valid - gdbmi.send_signal_to_gdb('SIGINT') # also valid - - raises ValueError if signal_input is invalie - raises NoGdbProcessError if there is no gdb process to send a signal to + These are all valid ways to call this method: + ``` + gdbmi.send_signal_to_gdb(2) + gdbmi.send_signal_to_gdb('sigint') + gdbmi.send_signal_to_gdb('SIGINT') + ``` + + raises: + ValueError: if signal_input is invalid + NoGdbProcessError: if there is no gdb process to send a signal to """ try: signal = int(signal_input) @@ -432,9 +439,8 @@ def interrupt_gdb(self): """Send SIGINT (interrupt signal) to the gdb subprocess""" self.send_signal_to_gdb("SIGINT") - def exit(self): - """Terminate gdb process - Returns: None""" + def exit(self) -> None: + """Terminate gdb process""" if self.gdb_process: self.gdb_process.terminate() self.gdb_process.communicate() diff --git a/pygdbmi/gdbmiparser.py b/pygdbmi/gdbmiparser.py index f82bb15..9787a17 100755 --- a/pygdbmi/gdbmiparser.py +++ b/pygdbmi/gdbmiparser.py @@ -1,16 +1,16 @@ """ Python parser for gdb's machine interface interpreter. -Parses string output from gdb with the "--interpreter=mi2" flag into +Parses string output from gdb with the `--interpreter=mi2` flag into structured objects. See more at https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html#GDB_002fMI - """ import logging import re from pprint import pprint +from typing import Dict from pygdbmi.printcolor import fmt_green from pygdbmi.StringStream import StringStream @@ -38,20 +38,17 @@ def _setup_logger(logger, debug): _setup_logger(logger, _DEBUG) -def parse_response(gdb_mi_text): +def parse_response(gdb_mi_text: str) -> Dict: """Parse gdb mi text and turn it into a dictionary. See https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Stream-Records.html#GDB_002fMI-Stream-Records for details on types of gdb mi output. Args: - gdb_mi_text (str): String output from gdb + gdb_mi_text: String output from gdb Returns: - dict with the following keys: - type (either 'notify', 'result', 'console', 'log', 'target', 'done'), - message (str or None), - payload (str, list, dict, or None) + dictionary with keys "type", "message", "payload", "token" """ stream = StringStream(gdb_mi_text, debug=_DEBUG) @@ -74,25 +71,32 @@ def parse_response(gdb_mi_text): } elif _GDB_MI_CONSOLE_RE.match(gdb_mi_text): + match = _GDB_MI_CONSOLE_RE.match(gdb_mi_text) + if match: + payload = match.groups()[0] + else: + payload = None return { "type": "console", "message": None, - "payload": _GDB_MI_CONSOLE_RE.match(gdb_mi_text).groups()[0], + "payload": payload, } elif _GDB_MI_LOG_RE.match(gdb_mi_text): - return { - "type": "log", - "message": None, - "payload": _GDB_MI_LOG_RE.match(gdb_mi_text).groups()[0], - } + match = _GDB_MI_LOG_RE.match(gdb_mi_text) + if match: + payload = match.groups()[0] + else: + payload = None + return {"type": "log", "message": None, "payload": payload} elif _GDB_MI_TARGET_OUTPUT_RE.match(gdb_mi_text): - return { - "type": "target", - "message": None, - "payload": _GDB_MI_TARGET_OUTPUT_RE.match(gdb_mi_text).groups()[0], - } + match = _GDB_MI_TARGET_OUTPUT_RE.match(gdb_mi_text) + if match: + payload = match.groups()[0] + else: + payload = None + return {"type": "target", "message": None, "payload": payload} elif response_is_finished(gdb_mi_text): return {"type": "done", "message": None, "payload": None} @@ -103,9 +107,15 @@ def parse_response(gdb_mi_text): return {"type": "output", "message": None, "payload": gdb_mi_text} -def response_is_finished(gdb_mi_text): +def response_is_finished(gdb_mi_text: str) -> bool: """Return true if the gdb mi response is ending - Returns: True if gdb response is finished""" + + Args: + gdb_mi_text: String output from gdb + + Returns: + True if gdb response is finished + """ if _GDB_MI_RESPONSE_FINISHED_RE.match(gdb_mi_text): return True @@ -114,9 +124,9 @@ def response_is_finished(gdb_mi_text): def assert_match(actual_char_or_str, expected_char_or_str): - """If values don't match, print them and raise a ValueError, otherwise, - continue - Raises: ValueError if arguments do not match""" + # Skip: If values don't match, print them and raise a ValueError, otherwise, + # continue + # Raises: ValueError if arguments do not match""" if expected_char_or_str != actual_char_or_str: print("Expected") pprint(expected_char_or_str) diff --git a/pygdbmi/py.typed b/pygdbmi/py.typed new file mode 100644 index 0000000..b9e8e01 --- /dev/null +++ b/pygdbmi/py.typed @@ -0,0 +1,2 @@ +# Marker file for PEP 561. This package uses inline types. +# https://mypy.readthedocs.io/en/latest/installed_packages.html#making-pep-561-compatible-packages \ No newline at end of file